swagger,swagger-beauty,security,rbac-shiro
spring-boot-demo-swagger
集成原生 swagger ,自动生成 API 文档。>
启动项目,访问地址:http://localhost:8080/demo/swagger-ui.html#/
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><artifactId>spring-boot-demo-swagger</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><parent><groupId>com.xkcoding</groupId><artifactId>spring-boot-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><swagger.version>2.9.2</swagger.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>${swagger.version}</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>${swagger.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><finalName>spring-boot-demo-swagger</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
Swagger2Config.java
@Configuration
@EnableSwagger2
public class Swagger2Config {@Beanpublic Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.xkcoding.swagger.controller")).paths(PathSelectors.any()).build();}private ApiInfo apiInfo() {return new ApiInfoBuilder().title("spring-boot-demo").description("这是一个简单的 Swagger API 演示").contact(new Contact("Yangkai.Shen", "http://xkcoding.com", "237497819@qq.com")).version("1.0.0-SNAPSHOT").build();}
}
UserController.java
主要演示API层的注解。
@RestController
@RequestMapping("/user")
@Api(tags = "1.0.0-SNAPSHOT", description = "用户管理", value = "用户管理")
@Slf4j
public class UserController {@GetMapping@ApiOperation(value = "条件查询(DONE)", notes = "备注")@ApiImplicitParams({@ApiImplicitParam(name = "username", value = "用户名", dataType = DataType.STRING, paramType = ParamType.QUERY, defaultValue = "xxx")})public ApiResponse<User> getByUserName(String username) {log.info("多个参数用 @ApiImplicitParams");return ApiResponse.<User>builder().code(200).message("操作成功").data(new User(1, username, "JAVA")).build();}@GetMapping("/{id}")@ApiOperation(value = "主键查询(DONE)", notes = "备注")@ApiImplicitParams({@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)})public ApiResponse<User> get(@PathVariable Integer id) {log.info("单个参数用 @ApiImplicitParam");return ApiResponse.<User>builder().code(200).message("操作成功").data(new User(id, "u1", "p1")).build();}@DeleteMapping("/{id}")@ApiOperation(value = "删除用户(DONE)", notes = "备注")@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)public void delete(@PathVariable Integer id) {log.info("单个参数用 ApiImplicitParam");}@PostMapping@ApiOperation(value = "添加用户(DONE)")public User post(@RequestBody User user) {log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");return user;}@PostMapping("/multipar")@ApiOperation(value = "添加用户(DONE)")public List<User> multipar(@RequestBody List<User> user) {log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");return user;}@PostMapping("/array")@ApiOperation(value = "添加用户(DONE)")public User[] array(@RequestBody User[] user) {log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");return user;}@PutMapping("/{id}")@ApiOperation(value = "修改用户(DONE)")public void put(@PathVariable Long id, @RequestBody User user) {log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 ");}@PostMapping("/{id}/file")@ApiOperation(value = "文件上传(DONE)")public String file(@PathVariable Long id, @RequestParam("file") MultipartFile file) {log.info(file.getContentType());log.info(file.getName());log.info(file.getOriginalFilename());return file.getOriginalFilename();}
}
ApiResponse.java
主要演示了 实体类 的注解。
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "通用PI接口返回", description = "Common Api Response")
public class ApiResponse<T> implements Serializable {private static final long serialVersionUID = -8987146499044811408L;@ApiModelProperty(value = "通用返回状态", required = true)private Integer code;@ApiModelProperty(value = "通用返回信息", required = true)private String message;@ApiModelProperty(value = "通用返回数据", required = true)private T data;
}
参考
- swagger 官方网站:https://swagger.io/
- swagger 官方文档:https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X—Getting-started
- swagger 常用注解:https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X—Annotations
spring-boot-demo-swagger-beauty
(https://github.com/battcn/swagger-spring-boot) 集成。
启动项目,访问地址:http://localhost:8080/demo/swagger-ui.html#/
用户名:xkcoding
密码:123456
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><artifactId>spring-boot-demo-swagger-beauty</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><parent><groupId>com.xkcoding</groupId><artifactId>spring-boot-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><battcn.swagger.version>2.1.2-RELEASE</battcn.swagger.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.battcn</groupId><artifactId>swagger-spring-boot-starter</artifactId><version>${battcn.swagger.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><finalName>spring-boot-demo-swagger-beauty</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
application.yml
server:port: 8080servlet:context-path: /demo
spring:swagger:enabled: truetitle: spring-boot-demodescription: 这是一个简单的 Swagger API 演示version: 1.0.0-SNAPSHOTcontact:name: Yangkai.Shenemail: 237497819@qq.comurl: http://xkcoding.com# swagger扫描的基础包,默认:全扫描# base-package:# 需要处理的基础URL规则,默认:/# base-path:# 需要排除的URL规则,默认:空# exclude-path:security:# 是否启用 swagger 登录验证filter-plugin: trueusername: xkcodingpassword: 123456global-response-messages:GET[0]:code: 400message: Bad Request,一般为请求参数不对GET[1]:code: 404message: NOT FOUND,一般为请求路径不对GET[2]:code: 500message: ERROR,一般为程序内部错误POST[0]:code: 400message: Bad Request,一般为请求参数不对POST[1]:code: 404message: NOT FOUND,一般为请求路径不对POST[2]:code: 500message: ERROR,一般为程序内部错误
ApiResponse.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "通用PI接口返回", description = "Common Api Response")
public class ApiResponse<T> implements Serializable {private static final long serialVersionUID = -8987146499044811408L;@ApiModelProperty(value = "通用返回状态", required = true)private Integer code;@ApiModelProperty(value = "通用返回信息", required = true)private String message;@ApiModelProperty(value = "通用返回数据", required = true)private T data;
}
User.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用户实体", description = "User Entity")
public class User implements Serializable {private static final long serialVersionUID = 5057954049311281252L;@ApiModelProperty(value = "主键id", required = true)private Integer id;@ApiModelProperty(value = "用户名", required = true)private String name;@ApiModelProperty(value = "工作岗位", required = true)private String job;
}
UserController.java
@RestController
@RequestMapping("/user")
@Api(tags = "1.0.0-SNAPSHOT", description = "用户管理", value = "用户管理")
@Slf4j
public class UserController {@GetMapping@ApiOperation(value = "条件查询(DONE)", notes = "备注")@ApiImplicitParams({@ApiImplicitParam(name = "username", value = "用户名", dataType = DataType.STRING, paramType = ParamType.QUERY, defaultValue = "xxx")})public ApiResponse<User> getByUserName(String username) {log.info("多个参数用 @ApiImplicitParams");return ApiResponse.<User>builder().code(200).message("操作成功").data(new User(1, username, "JAVA")).build();}@GetMapping("/{id}")@ApiOperation(value = "主键查询(DONE)", notes = "备注")@ApiImplicitParams({@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)})public ApiResponse<User> get(@PathVariable Integer id) {log.info("单个参数用 @ApiImplicitParam");return ApiResponse.<User>builder().code(200).message("操作成功").data(new User(id, "u1", "p1")).build();}@DeleteMapping("/{id}")@ApiOperation(value = "删除用户(DONE)", notes = "备注")@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)public void delete(@PathVariable Integer id) {log.info("单个参数用 ApiImplicitParam");}@PostMapping@ApiOperation(value = "添加用户(DONE)")public User post(@RequestBody User user) {log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");return user;}@PostMapping("/multipar")@ApiOperation(value = "添加用户(DONE)")public List<User> multipar(@RequestBody List<User> user) {log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");return user;}@PostMapping("/array")@ApiOperation(value = "添加用户(DONE)")public User[] array(@RequestBody User[] user) {log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");return user;}@PutMapping("/{id}")@ApiOperation(value = "修改用户(DONE)")public void put(@PathVariable Long id, @RequestBody User user) {log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 ");}@PostMapping("/{id}/file")@ApiOperation(value = "文件上传(DONE)")public String file(@PathVariable Long id, @RequestParam("file") MultipartFile file) {log.info(file.getContentType());log.info(file.getName());log.info(file.getOriginalFilename());return file.getOriginalFilename();}
}
参考
- https://github.com/battcn/swagger-spring-boot/blob/master/README.md
- 几款比较好看的swagger-ui,具体使用方法参见各个依赖的官方文档:
- battcn 的 swagger-spring-boot-starter 文档:https://github.com/battcn/swagger-spring-boot/blob/master/README.md
- swagger-ui-layer 文档:https://gitee.com/caspar-chen/Swagger-UI-layer#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8
- swagger-bootstrap-ui 文档:https://gitee.com/xiaoym/swagger-bootstrap-ui#%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
- swagger-ui-themes 文档:https://github.com/ostranme/swagger-ui-themes#getting-started
spring-boot-demo-rbac-security
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><artifactId>spring-boot-demo-rbac-security</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><parent><groupId>com.xkcoding</groupId><artifactId>spring-boot-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><jjwt.veersion>0.9.1</jjwt.veersion></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- 对象池,使用redis时必须引入 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>${jjwt.veersion}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><finalName>spring-boot-demo-rbac-security</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
3.2. JwtUtil.java
JWT 工具类,主要功能:生成JWT并存入Redis、解析JWT并校验其准确性、从Request的Header中获取JWT
@EnableConfigurationProperties(JwtConfig.class)
@Configuration
@Slf4j
public class JwtUtil {@Autowiredprivate JwtConfig jwtConfig;@Autowiredprivate StringRedisTemplate stringRedisTemplate;/* 创建JWT @param rememberMe 记住我* @param id 用户id* @param subject 用户名* @param roles 用户角色* @param authorities 用户权限* @return JWT*/public String createJWT(Boolean rememberMe, Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) {Date now = new Date();JwtBuilder builder = Jwts.builder().setId(id.toString()).setSubject(subject).setIssuedAt(now).signWith(SignatureAlgorithm.HS256, jwtConfig.getKey()).claim("roles", roles).claim("authorities", authorities);// 设置过期时间Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();if (ttl > 0) {builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));}String jwt = builder.compact();// 将生成的JWT保存至RedisstringRedisTemplate.opsForValue().set(Consts.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, TimeUnit.MILLISECONDS);return jwt;}/* 创建JWT @param authentication 用户认证信息* @param rememberMe 记住我* @return JWT*/public String createJWT(Authentication authentication, Boolean rememberMe) {UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities());}/* 解析JWT @param jwt JWT* @return {@link Claims}*/public Claims parseJWT(String jwt) {try {Claims claims = Jwts.parser().setSigningKey(jwtConfig.getKey()).parseClaimsJws(jwt).getBody();String username = claims.getSubject();String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username;// 校验redis中的JWT是否存在Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);if (Objects.isNull(expire) || expire <= 0) {throw new SecurityException(Status.TOKEN_EXPIRED);}// 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期String redisToken = stringRedisTemplate.opsForValue().get(redisKey);if (!StrUtil.equals(jwt, redisToken)) {throw new SecurityException(Status.TOKEN_OUT_OF_CTRL);}return claims;} catch (ExpiredJwtException e) {log.error("Token 已过期");throw new SecurityException(Status.TOKEN_EXPIRED);} catch (UnsupportedJwtException e) {log.error("不支持的 Token");throw new SecurityException(Status.TOKEN_PARSE_ERROR);} catch (MalformedJwtException e) {log.error("Token 无效");throw new SecurityException(Status.TOKEN_PARSE_ERROR);} catch (SignatureException e) {log.error("无效的 Token 签名");throw new SecurityException(Status.TOKEN_PARSE_ERROR);} catch (IllegalArgumentException e) {log.error("Token 参数不存在");throw new SecurityException(Status.TOKEN_PARSE_ERROR);}}/* 设置JWT过期 @param request 请求*/public void invalidateJWT(HttpServletRequest request) {String jwt = getJwtFromRequest(request);String username = getUsernameFromJWT(jwt);// 从redis中清除JWTstringRedisTemplate.delete(Consts.REDIS_JWT_KEY_PREFIX + username);}/* 根据 jwt 获取用户名 @param jwt JWT* @return 用户名*/public String getUsernameFromJWT(String jwt) {Claims claims = parseJWT(jwt);return claims.getSubject();}/* 从 request 的 header 中获取 JWT @param request 请求* @return JWT*/public String getJwtFromRequest(HttpServletRequest request) {String bearerToken = request.getHeader("Authorization");if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7);}return null;}}
3.3. SecurityConfig.java
Spring Security 配置类,主要功能:配置哪些URL不需要认证,哪些需要认证
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(CustomConfig.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate CustomConfig customConfig;@Autowiredprivate AccessDeniedHandler accessDeniedHandler;@Autowiredprivate CustomUserDetailsService customUserDetailsService;@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic BCryptPasswordEncoder encoder() {return new BCryptPasswordEncoder();}@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(customUserDetailsService).passwordEncoder(encoder());}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors()// 关闭 CSRF.and().csrf().disable()// 登录行为由自己实现,参考 AuthController#login.formLogin().disable().httpBasic().disable()// 认证请求.authorizeRequests()// 所有请求都需要登录访问.anyRequest().authenticated()// RBAC 动态 url 认证.anyRequest().access("@rbacAuthorityService.hasPermission(request,authentication)")// 登出行为由自己实现,参考 AuthController#logout.and().logout().disable()// Session 管理.sessionManagement()// 因为使用了JWT,所以这里不管理Session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 异常处理.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler);// 添加自定义 JWT 过滤器http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}/* 放行所有不需要登录就可以访问的请求,参见 AuthController* 也可以在 {@link #configure(HttpSecurity)} 中配置* {@code http.authorizeRequests().antMatchers("/api/auth/").permitAll()}*/@Overridepublic void configure(WebSecurity web) {WebSecurity and = web.ignoring().and();// 忽略 GETcustomConfig.getIgnores().getGet().forEach(url -> and.ignoring().antMatchers(HttpMethod.GET, url));// 忽略 POSTcustomConfig.getIgnores().getPost().forEach(url -> and.ignoring().antMatchers(HttpMethod.POST, url));// 忽略 DELETEcustomConfig.getIgnores().getDelete().forEach(url -> and.ignoring().antMatchers(HttpMethod.DELETE, url));// 忽略 PUTcustomConfig.getIgnores().getPut().forEach(url -> and.ignoring().antMatchers(HttpMethod.PUT, url));// 忽略 HEADcustomConfig.getIgnores().getHead().forEach(url -> and.ignoring().antMatchers(HttpMethod.HEAD, url));// 忽略 PATCHcustomConfig.getIgnores().getPatch().forEach(url -> and.ignoring().antMatchers(HttpMethod.PATCH, url));// 忽略 OPTIONScustomConfig.getIgnores().getOptions().forEach(url -> and.ignoring().antMatchers(HttpMethod.OPTIONS, url));// 忽略 TRACEcustomConfig.getIgnores().getTrace().forEach(url -> and.ignoring().antMatchers(HttpMethod.TRACE, url));// 按照请求格式忽略customConfig.getIgnores().getPattern().forEach(url -> and.ignoring().antMatchers(url));}
}
3.4. RbacAuthorityService.java
/* <p>* 动态路由认证* </p> @author yangkai.shen* @date Created in 2018-12-10 17:17*/
@Component
public class RbacAuthorityService {@Autowiredprivate RoleDao roleDao;@Autowiredprivate PermissionDao permissionDao;@Autowiredprivate RequestMappingHandlerMapping mapping;public boolean hasPermission(HttpServletRequest request, Authentication authentication) {checkRequest(request);Object userInfo = authentication.getPrincipal();boolean hasPermission = false;if (userInfo instanceof UserDetails) {UserPrincipal principal = (UserPrincipal) userInfo;Long userId = principal.getId();List<Role> roles = roleDao.selectByUserId(userId);List<Long> roleIds = roles.stream().map(Role::getId).collect(Collectors.toList());List<Permission> permissions = permissionDao.selectByRoleIdList(roleIds);//获取资源,前后端分离,所以过滤页面权限,只保留按钮权限List<Permission> btnPerms = permissions.stream()// 过滤页面权限.filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON))// 过滤 URL 为空.filter(permission -> StrUtil.isNotBlank(permission.getUrl()))// 过滤 METHOD 为空.filter(permission -> StrUtil.isNotBlank(permission.getMethod())).collect(Collectors.toList());for (Permission btnPerm : btnPerms) {AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod());if (antPathMatcher.matches(request)) {hasPermission = true;break;}}return hasPermission;} else {return false;}}/* 校验请求是否存在 @param request 请求*/private void checkRequest(HttpServletRequest request) {// 获取当前 request 的方法String currentMethod = request.getMethod();Multimap<String, String> urlMapping = allUrlMapping();for (String uri : urlMapping.keySet()) {// 通过 AntPathRequestMatcher 匹配 url// 可以通过 2 种方式创建 AntPathRequestMatcher// 1:new AntPathRequestMatcher(uri,method) 这种方式可以直接判断方法是否匹配,因为这里我们把 方法不匹配 自定义抛出,所以,我们使用第2种方式创建// 2:new AntPathRequestMatcher(uri) 这种方式不校验请求方法,只校验请求路径AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri);if (antPathMatcher.matches(request)) {if (!urlMapping.get(uri).contains(currentMethod)) {throw new SecurityException(Status.HTTP_BAD_METHOD);} else {return;}}}throw new SecurityException(Status.REQUEST_NOT_FOUND);}/* 获取 所有URL Mapping,返回格式为{"/test":["GET","POST"],"/sys":["GET","DELETE"]} @return {@link ArrayListMultimap} 格式的 URL Mapping*/private Multimap<String, String> allUrlMapping() {Multimap<String, String> urlMapping = ArrayListMultimap.create();// 获取url与类和方法的对应信息Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();handlerMethods.forEach((k, v) -> {// 获取当前 key 下的获取所有URLSet<String> url = k.getPatternsCondition().getPatterns();RequestMethodsRequestCondition method = k.getMethodsCondition();// 为每个URL添加所有的请求方法url.forEach(s -> urlMapping.putAll(s, method.getMethods().stream().map(Enum::toString).collect(Collectors.toList())));});return urlMapping;}
}
3.5. JwtAuthenticationFilter.java
/* <p>* Jwt 认证过滤器* </p> @author yangkai.shen* @date Created in 2018-12-10 15:15*/
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate CustomUserDetailsService customUserDetailsService;@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate CustomConfig customConfig;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {if (checkIgnores(request)) {filterChain.doFilter(request, response);return;}String jwt = jwtUtil.getJwtFromRequest(request);if (StrUtil.isNotBlank(jwt)) {try {String username = jwtUtil.getUsernameFromJWT(jwt);UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);filterChain.doFilter(request, response);} catch (SecurityException e) {ResponseUtil.renderJson(response, e);}} else {ResponseUtil.renderJson(response, Status.UNAUTHORIZED, null);}}/* 请求是否不需要进行权限拦截 @param request 当前请求* @return true - 忽略,false - 不忽略*/private boolean checkIgnores(HttpServletRequest request) {String method = request.getMethod();HttpMethod httpMethod = HttpMethod.resolve(method);if (ObjectUtil.isNull(httpMethod)) {httpMethod = HttpMethod.GET;}Set<String> ignores = Sets.newHashSet();switch (httpMethod) {case GET:ignores.addAll(customConfig.getIgnores().getGet());break;case PUT:ignores.addAll(customConfig.getIgnores().getPut());break;case HEAD:ignores.addAll(customConfig.getIgnores().getHead());break;case POST:ignores.addAll(customConfig.getIgnores().getPost());break;case PATCH:ignores.addAll(customConfig.getIgnores().getPatch());break;case TRACE:ignores.addAll(customConfig.getIgnores().getTrace());break;case DELETE:ignores.addAll(customConfig.getIgnores().getDelete());break;case OPTIONS:ignores.addAll(customConfig.getIgnores().getOptions());break;default:break;}ignores.addAll(customConfig.getIgnores().getPattern());if (CollUtil.isNotEmpty(ignores)) {for (String ignore : ignores) {AntPathRequestMatcher matcher = new AntPathRequestMatcher(ignore, method);if (matcher.matches(request)) {return true;}}}return false;}}
3.6. CustomUserDetailsService.java
实现
UserDetailsService
接口,主要功能:根据用户名查询用户信息
/* <p>* 自定义UserDetails查询* </p> @author yangkai.shen* @date Created in 2018-12-10 10:29*/
@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserDao userDao;@Autowiredprivate RoleDao roleDao;@Autowiredprivate PermissionDao permissionDao;@Overridepublic UserDetails loadUserByUsername(String usernameOrEmailOrPhone) throws UsernameNotFoundException {User user = userDao.findByUsernameOrEmailOrPhone(usernameOrEmailOrPhone, usernameOrEmailOrPhone, usernameOrEmailOrPhone).orElseThrow(() -> new UsernameNotFoundException("未找到用户信息 : " + usernameOrEmailOrPhone));List<Role> roles = roleDao.selectByUserId(user.getId());List<Long> roleIds = roles.stream().map(Role::getId).collect(Collectors.toList());List<Permission> permissions = permissionDao.selectByRoleIdList(roleIds);return UserPrincipal.create(user, roles, permissions);}
}
3.7. RedisUtil.java
主要功能:根据key的格式分页获取Redis存在的key列表
@Component
@Slf4j
public class RedisUtil {@Autowiredprivate StringRedisTemplate stringRedisTemplate;/* 分页获取指定格式key,使用 scan 命令代替 keys 命令,在大数据量的情况下可以提高查询效率 @param patternKey key格式* @param currentPage 当前页码* @param pageSize 每页条数* @return 分页获取指定格式key*/public PageResult<String> findKeysForPage(String patternKey, int currentPage, int pageSize) {ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory();RedisConnection rc = factory.getConnection();Cursor<byte[]> cursor = rc.scan(options);List<String> result = Lists.newArrayList();long tmpIndex = 0;int startIndex = (currentPage - 1) * pageSize;int end = currentPage * pageSize;while (cursor.hasNext()) {String key = new String(cursor.next());if (tmpIndex >= startIndex && tmpIndex < end) {result.add(key);}tmpIndex++;}try {cursor.close();RedisConnectionUtils.releaseConnection(rc, factory);} catch (Exception e) {log.warn("Redis连接关闭异常,", e);}return new PageResult<>(result, tmpIndex);}
}
3.8. MonitorService.java
监控服务,主要功能:查询当前在线人数分页列表,手动踢出某个用户
package com.xkcoding.rbac.security.service;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import com.xkcoding.rbac.security.common.Consts;
import com.xkcoding.rbac.security.common.PageResult;
import com.xkcoding.rbac.security.model.User;
import com.xkcoding.rbac.security.repository.UserDao;
import com.xkcoding.rbac.security.util.RedisUtil;
import com.xkcoding.rbac.security.vo.OnlineUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;
import java.util.stream.Collectors;/* <p>* 监控 Service* </p> @author yangkai.shen* @date Created in 2018-12-12 00:55*/
@Service
public class MonitorService {@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate UserDao userDao;public PageResult<OnlineUser> onlineUser(Integer page, Integer size) {PageResult<String> keys = redisUtil.findKeysForPage(Consts.REDIS_JWT_KEY_PREFIX + Consts.SYMBOL_STAR, page, size);List<String> rows = keys.getRows();Long total = keys.getTotal();// 根据 redis 中键获取用户名列表List<String> usernameList = rows.stream().map(s -> StrUtil.subAfter(s, Consts.REDIS_JWT_KEY_PREFIX, true)).collect(Collectors.toList());// 根据用户名查询用户信息List<User> userList = userDao.findByUsernameIn(usernameList);// 封装在线用户信息List<OnlineUser> onlineUserList = Lists.newArrayList();userList.forEach(user -> onlineUserList.add(OnlineUser.create(user)));return new PageResult<>(onlineUserList, total);}
}
4. 参考
- Spring Security 官方文档:https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/
- JWT 官网:https://jwt.io/
- JJWT开源工具参考:https://github.com/jwtk/jjwt#quickstart
- 授权部分参考官方文档:https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/#authorization
- 动态授权部分,参考博客:https://blog.csdn.net/larger5/article/details/81063438
rbac-shiro
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><artifactId>demo-rbac-shiro</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><parent><groupId>com.xkcoding</groupId><artifactId>spring-boot-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-undertow</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.1.0</version></dependency><!--执行 SQL 分析打印--><dependency><groupId>p6spy</groupId><artifactId>p6spy</artifactId><version>3.8.1</version></dependency><!--shiro--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-starter</artifactId><version>1.4.0</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><finalName>demo-rbac-shiro</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
yml
server:port: 8080servlet:context-path: /demo
spring:datasource:hikari:username: rootpassword: rootdriver-class-name: com.p6spy.engine.spy.P6SpyDriverurl: jdbc:p6spy:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
mybatis-plus:global-config:# 关闭bannerbanner: false
spy.properties
module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,batch,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
MybatisPlusConfig.java
@Configuration
public class MybatisPlusConfig {@Beanpublic PaginationInterceptor paginationInterceptor() {PaginationInterceptor paginationInterceptor = new PaginationInterceptor();List<ISqlParser> sqlParserList = new ArrayList<>();// 攻击 SQL 阻断解析器、加入解析链sqlParserList.add(new BlockAttackSqlParser());paginationInterceptor.setSqlParserList(sqlParserList);return paginationInterceptor;}/* SQL执行效率插件*/@Beanpublic PerformanceInterceptor performanceInterceptor() {return new PerformanceInterceptor();}
}