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+ * 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+ * 自定义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
@@ -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
+ * JWT 响应返回
+ *
+ * 自定义User
+ *