@@ -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, "操作成功"), | |||
/** | |||
* 操作异常 | |||
*/ | |||
@@ -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; | |||
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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
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; | |||
/** | |||
* <p> | |||
* 认证 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)); | |||
} | |||
} |
@@ -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 href; | |||
@@ -46,10 +46,15 @@ public class Permission { | |||
private Integer type; | |||
/** | |||
* 权限表达式 | |||
* 后端接口地址 | |||
*/ | |||
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.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<Permission, Long>, 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<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.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<Role, Long>, 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<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; | |||
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; | |||
/** | |||
* <p> | |||
@@ -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<String> roles, Collection<? extends GrantedAuthority> 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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |