diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java new file mode 100644 index 0000000..cc67517 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java @@ -0,0 +1,35 @@ +package com.xkcoding.rbac.security.common; + +/** + *

+ * 常量池 + *

+ * + * @package: com.xkcoding.rbac.security.common + * @description: 常量池 + * @author: yangkai.shen + * @date: Created in 2018-12-10 15:03 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +public interface Consts { + /** + * 启用 + */ + Integer ENABLE = 1; + /** + * 禁用 + */ + Integer DISABLE = 0; + + /** + * 页面 + */ + Integer PAGE = 1; + + /** + * 按钮 + */ + Integer BUTTON = 2; +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java index 6b0a196..f095edb 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java @@ -21,6 +21,7 @@ public enum Status implements IStatus { * 操作成功 */ SUCCESS(200, "操作成功"), + /** * 操作异常 */ @@ -29,7 +30,48 @@ public enum Status implements IStatus { /** * 退出成功 */ - LOGOUT(200, "退出成功"); + LOGOUT(200, "退出成功"), + + /** + * 暂无权限访问 + */ + ACCESS_DENIED(403, "权限不足"), + + /** + * 请求不存在 + */ + REQUEST_NOT_FOUND(404, "请求不存在"), + + /** + * 请求异常 + */ + BAD_REQUEST(400, "请求异常"), + + /** + * 参数不匹配 + */ + PARAM_NOT_MATCH(400, "参数不匹配"), + + /** + * 参数不能为空 + */ + PARAM_NOT_NULL(400,"参数不能为空"), + + /** + * 当前用户已被锁定,请联系管理员解锁! + */ + USER_DISABLED(403,"当前用户已被锁定,请联系管理员解锁!"), + + /** + * 用户名或密码错误 + */ + USERNAME_PASSWORD_ERROR(5001,"用户名或密码错误"), + + /** + * token 已过期,请重新登录 + */ + TOKEN_EXPIRED(5002,"token 已过期,请重新登录"), + TOKEN_PARSE_ERROR(5002,"token 解析失败,请尝试重新登录"); /** * 状态码 diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..ca50a5e --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java @@ -0,0 +1,82 @@ +package com.xkcoding.rbac.security.config; + +import cn.hutool.core.util.StrUtil; +import com.xkcoding.rbac.security.common.Status; +import com.xkcoding.rbac.security.exception.SecurityException; +import com.xkcoding.rbac.security.service.CustomUserDetailsService; +import com.xkcoding.rbac.security.util.JwtUtil; +import com.xkcoding.rbac.security.util.ResponseUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + *

+ * Jwt 认证过滤器 + *

+ * + * @package: com.xkcoding.rbac.security.config + * @description: Jwt 认证过滤器 + * @author: yangkai.shen + * @date: Created in 2018-12-10 15:15 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Component +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + @Autowired + private CustomUserDetailsService customUserDetailsService; + + @Autowired + private JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (antPathMatcher.match("/**/api/auth/**", request.getRequestURI())) { + filterChain.doFilter(request, response); + } else { + String jwt = 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.ACCESS_DENIED, null); + } + } + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java new file mode 100644 index 0000000..52b355d --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java @@ -0,0 +1,76 @@ +package com.xkcoding.rbac.security.config; + +import cn.hutool.core.util.StrUtil; +import com.xkcoding.rbac.security.common.Consts; +import com.xkcoding.rbac.security.model.Permission; +import com.xkcoding.rbac.security.model.Role; +import com.xkcoding.rbac.security.repository.PermissionDao; +import com.xkcoding.rbac.security.repository.RoleDao; +import com.xkcoding.rbac.security.vo.UserPrincipal; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * 动态路由认证 + *

+ * + * @package: com.xkcoding.rbac.security.config + * @description: 动态路由认证 + * @author: yangkai.shen + * @date: Created in 2018-12-10 17:17 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Component +public class RbacAuthorityService { + @Autowired + private RoleDao roleDao; + + @Autowired + private PermissionDao permissionDao; + + public boolean hasPermission(HttpServletRequest request, Authentication authentication) { + Object userInfo = authentication.getPrincipal(); + boolean hasPermission = false; + + if (userInfo instanceof UserDetails) { + UserPrincipal principal = (UserPrincipal) userInfo; + Long userId = principal.getId(); + + List roles = roleDao.selectByUserId(userId); + List roleIds = roles.stream() + .map(Role::getId) + .collect(Collectors.toList()); + List permissions = permissionDao.selectByRoleIdList(roleIds); + + //获取资源,前后端分离,所以过滤页面权限,只保留按钮权限 + List btnPerms = permissions.stream() + .filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON)) + .filter(permission -> StrUtil.isNotBlank(permission.getPermission())) + .filter(permission -> StrUtil.isNotBlank(permission.getMethod())) + .collect(Collectors.toList()); + + for (Permission btnPerm : btnPerms) { + AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getPermission(), btnPerm.getMethod()); + if (antPathMatcher.matches(request)) { + hasPermission = true; + break; + } + } + + return hasPermission; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java index 9013bd7..ebc8f60 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java @@ -1,11 +1,18 @@ package com.xkcoding.rbac.security.config; +import com.xkcoding.rbac.security.service.CustomUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -28,6 +35,32 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private LogoutSuccessHandler logoutSuccessHandler; + @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() @@ -37,27 +70,43 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { .csrf() .disable() + .formLogin() + .disable() + .httpBasic() + .disable() + // 认证请求 .authorizeRequests() // 放行 /api/auth/** 的所有请求,参见 AuthController - .antMatchers("/api/auth/**") + .antMatchers("/**/api/auth/**") .permitAll() .anyRequest() .authenticated() + // RBAC 动态 url 认证 + .anyRequest() + .access("@rbacAuthorityService.hasPermission(request,authentication)") // 登出处理 .and() .logout() // 登出请求默认为POST请求,改为GET请求 - .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")) + .logoutRequestMatcher(new AntPathRequestMatcher("/**/logout", "GET")) // 登出成功处理器 .logoutSuccessHandler(logoutSuccessHandler) .permitAll() - .and() // Session 管理 + .and() .sessionManagement() // 因为使用了JWT,所以这里不管理Session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + + // 异常处理 + .and() + .exceptionHandling() + .accessDeniedHandler(accessDeniedHandler); + + // 添加自定义 JWT 过滤器 + http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java index 83b95a7..cc7eb17 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java @@ -4,6 +4,7 @@ import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.util.ResponseUtil; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; /** @@ -31,4 +32,9 @@ public class SecurityHandlerConfig { public LogoutSuccessHandler logoutSuccessHandler() { return (request, response, authentication) -> ResponseUtil.renderJson(response, Status.LOGOUT, null); } + + @Bean + public AccessDeniedHandler accessDeniedHandler() { + return (request, response, accessDeniedException) -> ResponseUtil.renderJson(response, Status.ACCESS_DENIED, null); + } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/WebMvcConfig.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/WebMvcConfig.java new file mode 100644 index 0000000..cffb3fa --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/WebMvcConfig.java @@ -0,0 +1,32 @@ +package com.xkcoding.rbac.security.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + *

+ * MVC配置 + *

+ * + * @package: com.xkcoding.rbac.security.config + * @description: MVC配置 + * @author: yangkai.shen + * @date: Created in 2018-12-10 16:09 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private static final long MAX_AGE_SECS = 3600; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE") + .maxAge(MAX_AGE_SECS); + } +} \ No newline at end of file diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java index 9711043..db3605e 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java @@ -1,11 +1,22 @@ package com.xkcoding.rbac.security.controller; import com.xkcoding.rbac.security.common.ApiResponse; +import com.xkcoding.rbac.security.payload.LoginRequest; +import com.xkcoding.rbac.security.util.JwtUtil; +import com.xkcoding.rbac.security.vo.JwtResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.validation.Valid; + /** *

* 认证 Controller,包括用户注册,用户登录请求 @@ -24,11 +35,23 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/api/auth") public class AuthController { + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private JwtUtil jwtUtil; + /** * 登录 */ @PostMapping("/login") - public ApiResponse login() { - return ApiResponse.ofSuccess(); + public ApiResponse login(@Valid @RequestBody LoginRequest loginRequest) { + Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsernameOrEmailOrPhone(), loginRequest.getPassword())); + + SecurityContextHolder.getContext() + .setAuthentication(authentication); + + String jwt = jwtUtil.createJWT(authentication); + return ApiResponse.ofSuccess(new JwtResponse(jwt)); } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/TestController.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/TestController.java new file mode 100644 index 0000000..6c93ff9 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/TestController.java @@ -0,0 +1,38 @@ +package com.xkcoding.rbac.security.controller; + +import com.xkcoding.rbac.security.common.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 测试Controller + *

+ * + * @package: com.xkcoding.rbac.security.controller + * @description: 测试Controller + * @author: yangkai.shen + * @date: Created in 2018-12-10 15:44 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Slf4j +@RestController +@RequestMapping("/test") +public class TestController { + @GetMapping + public ApiResponse list() { + log.info("测试列表查询"); + return ApiResponse.ofMessage("测试列表查询"); + } + + @PostMapping + public ApiResponse add() { + log.info("测试列表添加"); + return ApiResponse.ofMessage("测试列表添加"); + } +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java new file mode 100644 index 0000000..884d643 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java @@ -0,0 +1,37 @@ +package com.xkcoding.rbac.security.exception; + +import com.xkcoding.rbac.security.common.BaseException; +import com.xkcoding.rbac.security.common.Status; +import lombok.Getter; + +/** + *

+ * 全局异常 + *

+ * + * @package: com.xkcoding.rbac.security.exception + * @description: 全局异常 + * @author: yangkai.shen + * @date: Created in 2018-12-10 17:24 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Getter +public class SecurityException extends BaseException { + public SecurityException(Status status) { + super(status); + } + + public SecurityException(Status status, Object data) { + super(status, data); + } + + public SecurityException(Integer code, String message) { + super(code, message); + } + + public SecurityException(Integer code, String message, Object data) { + super(code, message, data); + } +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..e38b98d --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,73 @@ +package com.xkcoding.rbac.security.exception.handler; + +import cn.hutool.core.collection.CollUtil; +import com.xkcoding.rbac.security.common.ApiResponse; +import com.xkcoding.rbac.security.common.BaseException; +import com.xkcoding.rbac.security.common.Status; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.validation.ConstraintViolationException; + +/** + *

+ * 全局统一异常处理 + *

+ * + * @package: com.xkcoding.rbac.security.exception.handler + * @description: 全局统一异常处理 + * @author: yangkai.shen + * @date: Created in 2018-12-10 17:00 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@ControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(value = Exception.class) + @ResponseBody + public ApiResponse handlerException(Exception e) { + if (e instanceof NoHandlerFoundException) { + log.error("【全局异常拦截】NoHandlerFoundException: 请求方法 {}, 请求路径 {}", ((NoHandlerFoundException) e).getRequestURL(), ((NoHandlerFoundException) e).getHttpMethod()); + return ApiResponse.ofStatus(Status.REQUEST_NOT_FOUND); + } else if (e instanceof MethodArgumentNotValidException) { + log.error("【全局异常拦截】MethodArgumentNotValidException", e); + return ApiResponse.of(Status.BAD_REQUEST.getCode(), ((MethodArgumentNotValidException) e).getBindingResult() + .getAllErrors() + .get(0) + .getDefaultMessage(), null); + } else if (e instanceof ConstraintViolationException) { + log.error("【全局异常拦截】ConstraintViolationException", e); + return ApiResponse.of(Status.BAD_REQUEST.getCode(), CollUtil.getFirst(((ConstraintViolationException) e).getConstraintViolations()) + .getMessage(), null); + } else if (e instanceof MethodArgumentTypeMismatchException) { + log.error("【全局异常拦截】MethodArgumentTypeMismatchException: 参数名 {}, 异常信息 {}", ((MethodArgumentTypeMismatchException) e).getName(), ((MethodArgumentTypeMismatchException) e).getMessage()); + return ApiResponse.ofStatus(Status.PARAM_NOT_MATCH); + } else if (e instanceof HttpMessageNotReadableException) { + log.error("【全局异常拦截】HttpMessageNotReadableException: 错误信息 {}", ((HttpMessageNotReadableException) e).getMessage()); + return ApiResponse.ofStatus(Status.PARAM_NOT_NULL); + } else if (e instanceof BadCredentialsException) { + log.error("【全局异常拦截】BadCredentialsException: 错误信息 {}", e.getMessage()); + return ApiResponse.ofStatus(Status.USERNAME_PASSWORD_ERROR); + } else if (e instanceof DisabledException) { + log.error("【全局异常拦截】BadCredentialsException: 错误信息 {}", e.getMessage()); + return ApiResponse.ofStatus(Status.USER_DISABLED); + } else if (e instanceof BaseException) { + log.error("【全局异常拦截】DataManagerException: 状态码 {}, 异常信息 {}", ((BaseException) e).getCode(), e.getMessage()); + return ApiResponse.ofException((BaseException) e); + } + + log.error("【全局异常拦截】: 异常信息 {} ", e.getMessage()); + return ApiResponse.ofStatus(Status.ERROR); + } +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/Permission.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/Permission.java index 93531c1..2f07b05 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/Permission.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/Permission.java @@ -36,7 +36,7 @@ public class Permission { private String name; /** - * 页面地址 + * 前端页面地址 */ private String href; @@ -46,10 +46,15 @@ public class Permission { private Integer type; /** - * 权限表达式 + * 后端接口地址 */ private String permission; + /** + * 后端接口访问方式 + */ + private String method; + /** * 排序 */ diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java new file mode 100644 index 0000000..eb473d0 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java @@ -0,0 +1,35 @@ +package com.xkcoding.rbac.security.payload; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + *

+ * 登录请求参数 + *

+ * + * @package: com.xkcoding.rbac.security.payload + * @description: 登录请求参数 + * @author: yangkai.shen + * @date: Created in 2018-12-10 15:52 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Data +public class LoginRequest { + + /** + * 用户名或邮箱或手机号 + */ + @NotBlank + private String usernameOrEmailOrPhone; + + /** + * 密码 + */ + @NotBlank + private String password; + +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/PermissionDao.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/PermissionDao.java index f8f30a7..6f7dc05 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/PermissionDao.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/PermissionDao.java @@ -4,6 +4,7 @@ import com.xkcoding.rbac.security.model.Permission; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; @@ -29,5 +30,5 @@ public interface PermissionDao extends JpaRepository, JpaSpeci * @return 权限列表 */ @Query(value = "SELECT DISTINCT sec_permission.* FROM sec_permission,sec_role,sec_role_permission WHERE sec_role.id = sec_role_permission.role_id AND sec_permission.id = sec_role_permission.permission_id AND sec_role.id IN (:ids)", nativeQuery = true) - List selectByRoleIdList(List ids); + List selectByRoleIdList(@Param("ids") List ids); } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/RoleDao.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/RoleDao.java index 8e8357c..0dba9e8 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/RoleDao.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/RoleDao.java @@ -4,6 +4,7 @@ import com.xkcoding.rbac.security.model.Role; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; @@ -28,5 +29,5 @@ public interface RoleDao extends JpaRepository, JpaSpecificationExec * @return 角色列表 */ @Query(value = "SELECT sec_role.* FROM sec_role,sec_user,sec_user_role WHERE sec_user.id = sec_user_role.user_id AND sec_role.id = sec_user_role.role_id AND sec_user.id = :userId", nativeQuery = true) - List selectByUserId(Long userId); + List selectByUserId(@Param("userId") Long userId); } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/CustomUserDetailsService.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/CustomUserDetailsService.java new file mode 100644 index 0000000..e153056 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/CustomUserDetailsService.java @@ -0,0 +1,54 @@ +package com.xkcoding.rbac.security.service; + +import com.xkcoding.rbac.security.model.Permission; +import com.xkcoding.rbac.security.model.Role; +import com.xkcoding.rbac.security.model.User; +import com.xkcoding.rbac.security.repository.PermissionDao; +import com.xkcoding.rbac.security.repository.RoleDao; +import com.xkcoding.rbac.security.repository.UserDao; +import com.xkcoding.rbac.security.vo.UserPrincipal; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 自定义UserDetails查询 + *

+ * + * @package: com.xkcoding.rbac.security.service + * @description: 自定义UserDetails查询 + * @author: yangkai.shen + * @date: Created in 2018-12-10 10:29 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@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 roles = roleDao.selectByUserId(user.getId()); + List roleIds = roles.stream() + .map(Role::getId) + .collect(Collectors.toList()); + List permissions = permissionDao.selectByRoleIdList(roleIds); + return UserPrincipal.create(user, roles, permissions); + } +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java index 4514b35..c66c398 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java @@ -1,14 +1,21 @@ package com.xkcoding.rbac.security.util; import cn.hutool.core.date.DateUtil; +import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.config.JwtConfig; +import com.xkcoding.rbac.security.exception.SecurityException; +import com.xkcoding.rbac.security.vo.UserPrincipal; import io.jsonwebtoken.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import java.util.Collection; import java.util.Date; +import java.util.List; /** *

@@ -37,19 +44,21 @@ public class JwtUtil { /** * 创建JWT * - * @param id 用户id - * @param subject 用户名 - * @param roles 用户角色 + * @param id 用户id + * @param subject 用户名 + * @param roles 用户角色 + * @param authorities 用户权限 * @return JWT */ - public String createJWT(String id, String subject, String roles) { + public String createJWT(Long id, String subject, List roles, Collection authorities) { Date now = new Date(); JwtBuilder builder = Jwts.builder() - .setId(id) + .setId(id.toString()) .setSubject(subject) .setIssuedAt(now) .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey()) - .claim("roles", roles); + .claim("roles", roles) + .claim("authorities", authorities); if (jwtConfig.getTtl() > 0) { builder.setExpiration(DateUtil.offsetMillisecond(now, jwtConfig.getTtl() .intValue())); @@ -57,6 +66,17 @@ public class JwtUtil { return builder.compact(); } + /** + * 创建JWT + * + * @param authentication 用户认证信息 + * @return JWT + */ + public String createJWT(Authentication authentication) { + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + return createJWT(userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities()); + } + /** * 解析JWT * @@ -64,24 +84,38 @@ public class JwtUtil { * @return {@link Claims} */ public Claims parseJWT(String jwt) { - Claims claims = null; try { - claims = Jwts.parser() + return Jwts.parser() .setSigningKey(jwtConfig.getKey()) .parseClaimsJws(jwt) .getBody(); } 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); } - return claims; + } + + /** + * 根据 jwt 获取用户名 + * + * @param jwt JWT + * @return 用户名 + */ + public String getUsernameFromJWT(String jwt) { + Claims claims = parseJWT(jwt); + return claims.getSubject(); } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java index f2fa4e4..0d9a879 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java @@ -2,6 +2,7 @@ package com.xkcoding.rbac.security.util; import cn.hutool.json.JSONUtil; import com.xkcoding.rbac.security.common.ApiResponse; +import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.IStatus; import lombok.extern.slf4j.Slf4j; @@ -44,4 +45,24 @@ public class ResponseUtil { log.error("Response写出JSON异常,", e); } } + + /** + * 往 response 写出 json + * + * @param response 响应 + * @param exception 异常 + */ + public static void renderJson(HttpServletResponse response, BaseException exception) { + try { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "*"); + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(200); + + response.getWriter() + .write(JSONUtil.toJsonStr(ApiResponse.ofException(exception))); + } catch (IOException e) { + log.error("Response写出JSON异常,", e); + } + } } \ No newline at end of file diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/JwtResponse.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/JwtResponse.java new file mode 100644 index 0000000..f403dd4 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/JwtResponse.java @@ -0,0 +1,36 @@ +package com.xkcoding.rbac.security.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + *

+ * JWT 响应返回 + *

+ * + * @package: com.xkcoding.rbac.security.vo + * @description: JWT 响应返回 + * @author: yangkai.shen + * @date: Created in 2018-12-10 16:01 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class JwtResponse { + /** + * token 字段 + */ + private String token; + /** + * token类型 + */ + private String tokenType = "Bearer"; + + public JwtResponse(String token) { + this.token = token; + } +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/UserPrincipal.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/UserPrincipal.java new file mode 100644 index 0000000..b244eae --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/UserPrincipal.java @@ -0,0 +1,151 @@ +package com.xkcoding.rbac.security.vo; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.xkcoding.rbac.security.common.Consts; +import com.xkcoding.rbac.security.model.Permission; +import com.xkcoding.rbac.security.model.Role; +import com.xkcoding.rbac.security.model.User; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * 自定义User + *

+ * + * @package: com.xkcoding.rbac.security.vo + * @description: 自定义User + * @author: yangkai.shen + * @date: Created in 2018-12-10 15:09 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserPrincipal implements UserDetails { + /** + * 主键 + */ + private Long id; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + @JsonIgnore + private String password; + + /** + * 昵称 + */ + private String nickname; + + /** + * 手机 + */ + private String phone; + + /** + * 邮箱 + */ + private String email; + + /** + * 生日 + */ + private Long birthday; + + /** + * 性别,男-1,女-2 + */ + private Integer sex; + + /** + * 状态,启用-1,禁用-0 + */ + private Integer status; + + /** + * 创建时间 + */ + private Long createTime; + + /** + * 更新时间 + */ + private Long updateTime; + + /** + * 用户角色列表 + */ + private List roles; + + /** + * 用户权限列表 + */ + private Collection authorities; + + public static UserPrincipal create(User user, List roles, List permissions) { + List roleNames = roles.stream() + .map(Role::getName) + .collect(Collectors.toList()); + + List authorities = permissions.stream() + .filter(permission -> StrUtil.isNotBlank(permission.getPermission())) + .map(permission -> new SimpleGrantedAuthority(permission.getPermission())) + .collect(Collectors.toList()); + + return new UserPrincipal(user.getId(), user.getUsername(), user.getPassword(), user.getNickname(), user.getPhone(), user.getEmail(), user.getBirthday(), user.getSex(), user.getStatus(), user.getCreateTime(), user.getUpdateTime(), roleNames, authorities); + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return Objects.equals(this.status, Consts.ENABLE); + } +} \ No newline at end of file