集成原生 swagger ,自动生成 API 文档。>
启动项目,访问地址:http://localhost:8080/demo/swagger-ui.html#/
<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.0modelVersion>
<artifactId>spring-boot-demo-swaggerartifactId>
<version>1.0.0-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>com.xkcodinggroupId>
<artifactId>spring-boot-demoartifactId>
<version>1.0.0-SNAPSHOTversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<swagger.version>2.9.2swagger.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>${swagger.version}version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>${swagger.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<finalName>spring-boot-demo-swaggerfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public 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", "[email protected]"))
.version("1.0.0-SNAPSHOT")
.build();
}
}
主要演示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();
}
}
主要演示了 实体类 的注解。
@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;
}
(https://github.com/battcn/swagger-spring-boot) 集成。
启动项目,访问地址:http://localhost:8080/demo/swagger-ui.html#/
用户名:xkcoding
密码:123456
<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.0modelVersion>
<artifactId>spring-boot-demo-swagger-beautyartifactId>
<version>1.0.0-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>com.xkcodinggroupId>
<artifactId>spring-boot-demoartifactId>
<version>1.0.0-SNAPSHOTversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<battcn.swagger.version>2.1.2-RELEASEbattcn.swagger.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.battcngroupId>
<artifactId>swagger-spring-boot-starterartifactId>
<version>${battcn.swagger.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<finalName>spring-boot-demo-swagger-beautyfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
server:
port: 8080
servlet:
context-path: /demo
spring:
swagger:
enabled: true
title: spring-boot-demo
description: 这是一个简单的 Swagger API 演示
version: 1.0.0-SNAPSHOT
contact:
name: Yangkai.Shen
email: [email protected]
url: http://xkcoding.com
# swagger扫描的基础包,默认:全扫描
# base-package:
# 需要处理的基础URL规则,默认:/**
# base-path:
# 需要排除的URL规则,默认:空
# exclude-path:
security:
# 是否启用 swagger 登录验证
filter-plugin: true
username: xkcoding
password: 123456
global-response-messages:
GET[0]:
code: 400
message: Bad Request,一般为请求参数不对
GET[1]:
code: 404
message: NOT FOUND,一般为请求路径不对
GET[2]:
code: 500
message: ERROR,一般为程序内部错误
POST[0]:
code: 400
message: Bad Request,一般为请求参数不对
POST[1]:
code: 404
message: NOT FOUND,一般为请求路径不对
POST[2]:
code: 500
message: ERROR,一般为程序内部错误
@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;
}
@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;
}
@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();
}
}
<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.0modelVersion>
<artifactId>spring-boot-demo-rbac-securityartifactId>
<version>1.0.0-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>com.xkcodinggroupId>
<artifactId>spring-boot-demoartifactId>
<version>1.0.0-SNAPSHOTversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<jjwt.veersion>0.9.1jjwt.veersion>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>${jjwt.veersion}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<finalName>spring-boot-demo-rbac-securityfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
JWT 工具类,主要功能:生成JWT并存入Redis、解析JWT并校验其准确性、从Request的Header中获取JWT
@EnableConfigurationProperties(JwtConfig.class)
@Configuration
@Slf4j
public class JwtUtil {
@Autowired
private JwtConfig jwtConfig;
@Autowired
private 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保存至Redis
stringRedisTemplate.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中清除JWT
stringRedisTemplate.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;
}
}
Spring Security 配置类,主要功能:配置哪些URL不需要认证,哪些需要认证
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(CustomConfig.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomConfig customConfig;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService)
.passwordEncoder(encoder());
}
@Override
protected 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()}
*/
@Override
public void configure(WebSecurity web) {
WebSecurity and = web.ignoring()
.and();
// 忽略 GET
customConfig.getIgnores()
.getGet()
.forEach(url -> and.ignoring()
.antMatchers(HttpMethod.GET, url));
// 忽略 POST
customConfig.getIgnores()
.getPost()
.forEach(url -> and.ignoring()
.antMatchers(HttpMethod.POST, url));
// 忽略 DELETE
customConfig.getIgnores()
.getDelete()
.forEach(url -> and.ignoring()
.antMatchers(HttpMethod.DELETE, url));
// 忽略 PUT
customConfig.getIgnores()
.getPut()
.forEach(url -> and.ignoring()
.antMatchers(HttpMethod.PUT, url));
// 忽略 HEAD
customConfig.getIgnores()
.getHead()
.forEach(url -> and.ignoring()
.antMatchers(HttpMethod.HEAD, url));
// 忽略 PATCH
customConfig.getIgnores()
.getPatch()
.forEach(url -> and.ignoring()
.antMatchers(HttpMethod.PATCH, url));
// 忽略 OPTIONS
customConfig.getIgnores()
.getOptions()
.forEach(url -> and.ignoring()
.antMatchers(HttpMethod.OPTIONS, url));
// 忽略 TRACE
customConfig.getIgnores()
.getTrace()
.forEach(url -> and.ignoring()
.antMatchers(HttpMethod.TRACE, url));
// 按照请求格式忽略
customConfig.getIgnores()
.getPattern()
.forEach(url -> and.ignoring()
.antMatchers(url));
}
}
/**
*
* 动态路由认证
*
*
* @author yangkai.shen
* @date Created in 2018-12-10 17:17
*/
@Component
public class RbacAuthorityService {
@Autowired
private RoleDao roleDao;
@Autowired
private PermissionDao permissionDao;
@Autowired
private 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 下的获取所有URL
Set<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;
}
}
/**
*
* Jwt 认证过滤器
*
*
* @author yangkai.shen
* @date Created in 2018-12-10 15:15
*/
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomConfig customConfig;
@Override
protected 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;
}
}
实现
UserDetailsService
接口,主要功能:根据用户名查询用户信息
/**
*
* 自定义UserDetails查询
*
*
* @author yangkai.shen
* @date Created in 2018-12-10 10:29
*/
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
@Autowired
private PermissionDao permissionDao;
@Override
public 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);
}
}
主要功能:根据key的格式分页获取Redis存在的key列表
@Component
@Slf4j
public class RedisUtil {
@Autowired
private 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);
}
}
监控服务,主要功能:查询当前在线人数分页列表,手动踢出某个用户
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;
/**
*
* 监控 Service
*
*
* @author yangkai.shen
* @date Created in 2018-12-12 00:55
*/
@Service
public class MonitorService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private 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);
}
}
<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.0modelVersion>
<artifactId>demo-rbac-shiroartifactId>
<version>1.0.0-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>com.xkcodinggroupId>
<artifactId>spring-boot-demoartifactId>
<version>1.0.0-SNAPSHOTversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-undertowartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>p6spygroupId>
<artifactId>p6spyartifactId>
<version>3.8.1version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-starterartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<finalName>demo-rbac-shirofinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
server:
port: 8080
servlet:
context-path: /demo
spring:
datasource:
hikari:
username: root
password: root
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: 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:
# 关闭banner
banner: false
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
@Configuration
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
// 攻击 SQL 阻断解析器、加入解析链
sqlParserList.add(new BlockAttackSqlParser());
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
/**
* SQL执行效率插件
*/
@Bean
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
}