@@ -0,0 +1,35 @@ | |||||
package com.xkcoding.rbac.security.common; | |||||
/** | |||||
* <p> | |||||
* 常量池 | |||||
* </p> | |||||
* | |||||
* @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; | |||||
} |
@@ -21,6 +21,7 @@ public enum Status implements IStatus { | |||||
* 操作成功 | * 操作成功 | ||||
*/ | */ | ||||
SUCCESS(200, "操作成功"), | 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 解析失败,请尝试重新登录"); | |||||
/** | /** | ||||
* 状态码 | * 状态码 | ||||
@@ -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; | |||||
/** | |||||
* <p> | |||||
* Jwt 认证过滤器 | |||||
* </p> | |||||
* | |||||
* @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; | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 动态路由认证 | |||||
* </p> | |||||
* | |||||
* @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<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)) | |||||
.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; | |||||
} | |||||
} | |||||
} |
@@ -1,11 +1,18 @@ | |||||
package com.xkcoding.rbac.security.config; | package com.xkcoding.rbac.security.config; | ||||
import com.xkcoding.rbac.security.service.CustomUserDetailsService; | |||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
import org.springframework.context.annotation.Bean; | |||||
import org.springframework.context.annotation.Configuration; | 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.builders.HttpSecurity; | ||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | ||||
import org.springframework.security.config.http.SessionCreationPolicy; | 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.authentication.logout.LogoutSuccessHandler; | ||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | ||||
@@ -28,6 +35,32 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { | |||||
@Autowired | @Autowired | ||||
private LogoutSuccessHandler logoutSuccessHandler; | 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 | @Override | ||||
protected void configure(HttpSecurity http) throws Exception { | protected void configure(HttpSecurity http) throws Exception { | ||||
http.cors() | http.cors() | ||||
@@ -37,27 +70,43 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { | |||||
.csrf() | .csrf() | ||||
.disable() | .disable() | ||||
.formLogin() | |||||
.disable() | |||||
.httpBasic() | |||||
.disable() | |||||
// 认证请求 | // 认证请求 | ||||
.authorizeRequests() | .authorizeRequests() | ||||
// 放行 /api/auth/** 的所有请求,参见 AuthController | // 放行 /api/auth/** 的所有请求,参见 AuthController | ||||
.antMatchers("/api/auth/**") | |||||
.antMatchers("/**/api/auth/**") | |||||
.permitAll() | .permitAll() | ||||
.anyRequest() | .anyRequest() | ||||
.authenticated() | .authenticated() | ||||
// RBAC 动态 url 认证 | |||||
.anyRequest() | |||||
.access("@rbacAuthorityService.hasPermission(request,authentication)") | |||||
// 登出处理 | // 登出处理 | ||||
.and() | .and() | ||||
.logout() | .logout() | ||||
// 登出请求默认为POST请求,改为GET请求 | // 登出请求默认为POST请求,改为GET请求 | ||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")) | |||||
.logoutRequestMatcher(new AntPathRequestMatcher("/**/logout", "GET")) | |||||
// 登出成功处理器 | // 登出成功处理器 | ||||
.logoutSuccessHandler(logoutSuccessHandler) | .logoutSuccessHandler(logoutSuccessHandler) | ||||
.permitAll() | .permitAll() | ||||
.and() | |||||
// Session 管理 | // Session 管理 | ||||
.and() | |||||
.sessionManagement() | .sessionManagement() | ||||
// 因为使用了JWT,所以这里不管理Session | // 因为使用了JWT,所以这里不管理Session | ||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); | |||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) | |||||
// 异常处理 | |||||
.and() | |||||
.exceptionHandling() | |||||
.accessDeniedHandler(accessDeniedHandler); | |||||
// 添加自定义 JWT 过滤器 | |||||
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); | |||||
} | } | ||||
} | } |
@@ -4,6 +4,7 @@ import com.xkcoding.rbac.security.common.Status; | |||||
import com.xkcoding.rbac.security.util.ResponseUtil; | import com.xkcoding.rbac.security.util.ResponseUtil; | ||||
import org.springframework.context.annotation.Bean; | import org.springframework.context.annotation.Bean; | ||||
import org.springframework.context.annotation.Configuration; | import org.springframework.context.annotation.Configuration; | ||||
import org.springframework.security.web.access.AccessDeniedHandler; | |||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; | ||||
/** | /** | ||||
@@ -31,4 +32,9 @@ public class SecurityHandlerConfig { | |||||
public LogoutSuccessHandler logoutSuccessHandler() { | public LogoutSuccessHandler logoutSuccessHandler() { | ||||
return (request, response, authentication) -> ResponseUtil.renderJson(response, Status.LOGOUT, null); | 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); | |||||
} | |||||
} | } |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* MVC配置 | |||||
* </p> | |||||
* | |||||
* @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); | |||||
} | |||||
} |
@@ -1,11 +1,22 @@ | |||||
package com.xkcoding.rbac.security.controller; | package com.xkcoding.rbac.security.controller; | ||||
import com.xkcoding.rbac.security.common.ApiResponse; | 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 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.PostMapping; | ||||
import org.springframework.web.bind.annotation.RequestBody; | |||||
import org.springframework.web.bind.annotation.RequestMapping; | import org.springframework.web.bind.annotation.RequestMapping; | ||||
import org.springframework.web.bind.annotation.RestController; | import org.springframework.web.bind.annotation.RestController; | ||||
import javax.validation.Valid; | |||||
/** | /** | ||||
* <p> | * <p> | ||||
* 认证 Controller,包括用户注册,用户登录请求 | * 认证 Controller,包括用户注册,用户登录请求 | ||||
@@ -24,11 +35,23 @@ import org.springframework.web.bind.annotation.RestController; | |||||
@RequestMapping("/api/auth") | @RequestMapping("/api/auth") | ||||
public class AuthController { | public class AuthController { | ||||
@Autowired | |||||
private AuthenticationManager authenticationManager; | |||||
@Autowired | |||||
private JwtUtil jwtUtil; | |||||
/** | /** | ||||
* 登录 | * 登录 | ||||
*/ | */ | ||||
@PostMapping("/login") | @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)); | |||||
} | } | ||||
} | } |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 测试Controller | |||||
* </p> | |||||
* | |||||
* @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("测试列表添加"); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 全局异常 | |||||
* </p> | |||||
* | |||||
* @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); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 全局统一异常处理 | |||||
* </p> | |||||
* | |||||
* @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); | |||||
} | |||||
} |
@@ -36,7 +36,7 @@ public class Permission { | |||||
private String name; | private String name; | ||||
/** | /** | ||||
* 页面地址 | |||||
* 前端页面地址 | |||||
*/ | */ | ||||
private String href; | private String href; | ||||
@@ -46,10 +46,15 @@ public class Permission { | |||||
private Integer type; | private Integer type; | ||||
/** | /** | ||||
* 权限表达式 | |||||
* 后端接口地址 | |||||
*/ | */ | ||||
private String permission; | private String permission; | ||||
/** | |||||
* 后端接口访问方式 | |||||
*/ | |||||
private String method; | |||||
/** | /** | ||||
* 排序 | * 排序 | ||||
*/ | */ | ||||
@@ -0,0 +1,35 @@ | |||||
package com.xkcoding.rbac.security.payload; | |||||
import lombok.Data; | |||||
import javax.validation.constraints.NotBlank; | |||||
/** | |||||
* <p> | |||||
* 登录请求参数 | |||||
* </p> | |||||
* | |||||
* @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; | |||||
} |
@@ -4,6 +4,7 @@ import com.xkcoding.rbac.security.model.Permission; | |||||
import org.springframework.data.jpa.repository.JpaRepository; | import org.springframework.data.jpa.repository.JpaRepository; | ||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; | ||||
import org.springframework.data.jpa.repository.Query; | import org.springframework.data.jpa.repository.Query; | ||||
import org.springframework.data.repository.query.Param; | |||||
import java.util.List; | import java.util.List; | ||||
@@ -29,5 +30,5 @@ public interface PermissionDao extends JpaRepository<Permission, Long>, JpaSpeci | |||||
* @return 权限列表 | * @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) | @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<Permission> selectByRoleIdList(List<Long> ids); | |||||
List<Permission> selectByRoleIdList(@Param("ids") List<Long> ids); | |||||
} | } |
@@ -4,6 +4,7 @@ import com.xkcoding.rbac.security.model.Role; | |||||
import org.springframework.data.jpa.repository.JpaRepository; | import org.springframework.data.jpa.repository.JpaRepository; | ||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; | ||||
import org.springframework.data.jpa.repository.Query; | import org.springframework.data.jpa.repository.Query; | ||||
import org.springframework.data.repository.query.Param; | |||||
import java.util.List; | import java.util.List; | ||||
@@ -28,5 +29,5 @@ public interface RoleDao extends JpaRepository<Role, Long>, JpaSpecificationExec | |||||
* @return 角色列表 | * @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) | @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<Role> selectByUserId(Long userId); | |||||
List<Role> selectByUserId(@Param("userId") Long userId); | |||||
} | } |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 自定义UserDetails查询 | |||||
* </p> | |||||
* | |||||
* @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<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); | |||||
} | |||||
} |
@@ -1,14 +1,21 @@ | |||||
package com.xkcoding.rbac.security.util; | package com.xkcoding.rbac.security.util; | ||||
import cn.hutool.core.date.DateUtil; | 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.config.JwtConfig; | ||||
import com.xkcoding.rbac.security.exception.SecurityException; | |||||
import com.xkcoding.rbac.security.vo.UserPrincipal; | |||||
import io.jsonwebtoken.*; | import io.jsonwebtoken.*; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
import org.springframework.boot.context.properties.EnableConfigurationProperties; | import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||||
import org.springframework.context.annotation.Configuration; | 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.Date; | ||||
import java.util.List; | |||||
/** | /** | ||||
* <p> | * <p> | ||||
@@ -37,19 +44,21 @@ public class JwtUtil { | |||||
/** | /** | ||||
* 创建JWT | * 创建JWT | ||||
* | * | ||||
* @param id 用户id | |||||
* @param subject 用户名 | |||||
* @param roles 用户角色 | |||||
* @param id 用户id | |||||
* @param subject 用户名 | |||||
* @param roles 用户角色 | |||||
* @param authorities 用户权限 | |||||
* @return JWT | * @return JWT | ||||
*/ | */ | ||||
public String createJWT(String id, String subject, String roles) { | |||||
public String createJWT(Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) { | |||||
Date now = new Date(); | Date now = new Date(); | ||||
JwtBuilder builder = Jwts.builder() | JwtBuilder builder = Jwts.builder() | ||||
.setId(id) | |||||
.setId(id.toString()) | |||||
.setSubject(subject) | .setSubject(subject) | ||||
.setIssuedAt(now) | .setIssuedAt(now) | ||||
.signWith(SignatureAlgorithm.HS256, jwtConfig.getKey()) | .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey()) | ||||
.claim("roles", roles); | |||||
.claim("roles", roles) | |||||
.claim("authorities", authorities); | |||||
if (jwtConfig.getTtl() > 0) { | if (jwtConfig.getTtl() > 0) { | ||||
builder.setExpiration(DateUtil.offsetMillisecond(now, jwtConfig.getTtl() | builder.setExpiration(DateUtil.offsetMillisecond(now, jwtConfig.getTtl() | ||||
.intValue())); | .intValue())); | ||||
@@ -57,6 +66,17 @@ public class JwtUtil { | |||||
return builder.compact(); | 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 | * 解析JWT | ||||
* | * | ||||
@@ -64,24 +84,38 @@ public class JwtUtil { | |||||
* @return {@link Claims} | * @return {@link Claims} | ||||
*/ | */ | ||||
public Claims parseJWT(String jwt) { | public Claims parseJWT(String jwt) { | ||||
Claims claims = null; | |||||
try { | try { | ||||
claims = Jwts.parser() | |||||
return Jwts.parser() | |||||
.setSigningKey(jwtConfig.getKey()) | .setSigningKey(jwtConfig.getKey()) | ||||
.parseClaimsJws(jwt) | .parseClaimsJws(jwt) | ||||
.getBody(); | .getBody(); | ||||
} catch (ExpiredJwtException e) { | } catch (ExpiredJwtException e) { | ||||
log.error("Token 已过期"); | log.error("Token 已过期"); | ||||
throw new SecurityException(Status.TOKEN_EXPIRED); | |||||
} catch (UnsupportedJwtException e) { | } catch (UnsupportedJwtException e) { | ||||
log.error("不支持的 Token"); | log.error("不支持的 Token"); | ||||
throw new SecurityException(Status.TOKEN_PARSE_ERROR); | |||||
} catch (MalformedJwtException e) { | } catch (MalformedJwtException e) { | ||||
log.error("Token 无效"); | log.error("Token 无效"); | ||||
throw new SecurityException(Status.TOKEN_PARSE_ERROR); | |||||
} catch (SignatureException e) { | } catch (SignatureException e) { | ||||
log.error("无效的 Token 签名"); | log.error("无效的 Token 签名"); | ||||
throw new SecurityException(Status.TOKEN_PARSE_ERROR); | |||||
} catch (IllegalArgumentException e) { | } catch (IllegalArgumentException e) { | ||||
log.error("Token 参数不存在"); | 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(); | |||||
} | } | ||||
} | } |
@@ -2,6 +2,7 @@ package com.xkcoding.rbac.security.util; | |||||
import cn.hutool.json.JSONUtil; | import cn.hutool.json.JSONUtil; | ||||
import com.xkcoding.rbac.security.common.ApiResponse; | import com.xkcoding.rbac.security.common.ApiResponse; | ||||
import com.xkcoding.rbac.security.common.BaseException; | |||||
import com.xkcoding.rbac.security.common.IStatus; | import com.xkcoding.rbac.security.common.IStatus; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
@@ -44,4 +45,24 @@ public class ResponseUtil { | |||||
log.error("Response写出JSON异常,", e); | 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); | |||||
} | |||||
} | |||||
} | } |
@@ -0,0 +1,36 @@ | |||||
package com.xkcoding.rbac.security.vo; | |||||
import lombok.AllArgsConstructor; | |||||
import lombok.Data; | |||||
import lombok.NoArgsConstructor; | |||||
/** | |||||
* <p> | |||||
* JWT 响应返回 | |||||
* </p> | |||||
* | |||||
* @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; | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 自定义User | |||||
* </p> | |||||
* | |||||
* @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<String> roles; | |||||
/** | |||||
* 用户权限列表 | |||||
*/ | |||||
private Collection<? extends GrantedAuthority> authorities; | |||||
public static UserPrincipal create(User user, List<Role> roles, List<Permission> permissions) { | |||||
List<String> roleNames = roles.stream() | |||||
.map(Role::getName) | |||||
.collect(Collectors.toList()); | |||||
List<GrantedAuthority> 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<? extends GrantedAuthority> 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); | |||||
} | |||||
} |