Spring Boot 项目如何集成 Spring Security 完成权限拦截操作。 为基于前后端分离的后端权限管理部分
关于JWT是什么,请参考JWT官网。这里就不多解释了,可理解为使用带签名的token来做用户和权限验证,现在流行的公共开放接口用的OAuth 2.0协议基本也是类似的套路。这里只是说下选择使用jwt不用session的原因。
首先,是要支持多端,一个api要支持H5, PC和APP三个前端,如果使用session的话对app不是很友好,而且session有跨域攻击的问题。
其次,后端的服务是无状态的,所以要支持分布式的权限校验。当然这个不是主要原因了,因为session持久化在spring里面也就是加一行注解就解决的问题。不过,spring通过代理httpsession来做,总归觉得有点复杂
关键流程
毫无疑问,对于spring框架使用最多的还是web系统。对于web系统来说进入认证的最佳入口就是Filter了。spring security不仅实现了认证的逻辑,还通过filter实现了常见的web攻击的防护。
常用Filter
下面按照request进入的顺序列举一下常用的Filter:
SecurityContext
放入Session的Filterpom.xml 添加配置
<jjwt.veersion>0.9.1jjwt.veersion>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>${jjwt.veersion}version>
dependency>
配置认证授权部分关键代码
UsernameLoginSecurityConfig 配置类,主要功能:配置哪些URL不需要认证,哪些需要认证
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(CustomConfig.class)
public class UsernameLoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomConfig customConfig;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Autowired
private LoginAuthenticationSuccessHandler usernameAuthenticationSuccessHandler;
@Autowired
private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;
@Autowired
private UserLoginService userLoginService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private BCryptPasswordEncoder encoder;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(buildLoginAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.cors()
// 关闭 CSRF
.and().csrf().disable()
// 登录行为由自己实现,参考 AuthController#login
.formLogin().disable()
.httpBasic().disable()
// 认证请求
.authorizeRequests()
// 所有请求都需要登录访问
.anyRequest()
.authenticated()
// RBAC 动态 url 认证
.anyRequest()
.access("@rbacAuthorityService.hasPermission(request,authentication)")
// 登出行为由自己实现,参考 AuthController#logout
.and().logout().disable()
// Session 管理
.sessionManagement()
// 因为使用了JWT,所以这里不管理Session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 异常处理
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler);
// @formatter:on
// 添加自定义 JWT 过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 放行所有不需要登录就可以访问的请求,参见 AuthController
* 也可以在 {@link #configure(HttpSecurity)} 中配置
* {@code http.authorizeRequests().antMatchers("/api/auth/**").permitAll()}
*/
@Override
public void configure(WebSecurity web) {
WebSecurity and = web.ignoring().and();
// 忽略 GET
customConfig.getIgnores().getGet().forEach(url -> and.ignoring().antMatchers(HttpMethod.GET, url));
// 忽略 POST
customConfig.getIgnores().getPost().forEach(url -> and.ignoring().antMatchers(HttpMethod.POST, url));
// 忽略 DELETE
customConfig.getIgnores().getDelete().forEach(url -> and.ignoring().antMatchers(HttpMethod.DELETE, url));
// 忽略 PUT
customConfig.getIgnores().getPut().forEach(url -> and.ignoring().antMatchers(HttpMethod.PUT, url));
// 忽略 HEAD
customConfig.getIgnores().getHead().forEach(url -> and.ignoring().antMatchers(HttpMethod.HEAD, url));
// 忽略 PATCH
customConfig.getIgnores().getPatch().forEach(url -> and.ignoring().antMatchers(HttpMethod.PATCH, url));
// 忽略 OPTIONS
customConfig.getIgnores().getOptions().forEach(url -> and.ignoring().antMatchers(HttpMethod.OPTIONS, url));
// 忽略 TRACE
customConfig.getIgnores().getTrace().forEach(url -> and.ignoring().antMatchers(HttpMethod.TRACE, url));
// 按照请求格式忽略
customConfig.getIgnores().getPattern().forEach(url -> and.ignoring().antMatchers(url));
}
@Bean
public UsernameLoginAuthenticationFilter buildLoginProcessingFilter() throws Exception {
UsernameLoginAuthenticationFilter filter = new UsernameLoginAuthenticationFilter();
filter.setAuthenticationSuccessHandler(usernameAuthenticationSuccessHandler);
filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
filter.setAuthenticationManager(super.authenticationManager());
return filter;
}
@Bean
public UsernameLoginAuthenticationProvider buildLoginAuthenticationProvider() {
UsernameLoginAuthenticationProvider provider = new UsernameLoginAuthenticationProvider();
provider.setUserLoginService(userLoginService);
provider.setEncoder(encoder);
return provider;
}
/**
* 退出时的处理策略配置
*
* @return logout success handler
*/
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new LogoutSuccessHandlerImpl();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JwtUtil.java
JWT 工具类,主要功能:生成JWT并存入Redis、解析JWT并校验其准确性、从Request的Header中获取JWT
/**
* @author : Lison
* @Date: 2019/10/28 14:35
* @Description: JWT 工具类
*/
@EnableConfigurationProperties(JwtConfig.class)
@Configuration
@Slf4j
public class JwtUtil {
@Autowired
private JwtConfig jwtConfig;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 创建JWT
*
* @param rememberMe 记住我
* @param id 用户id
* @param subject 用户名
* @param roles 用户角色
* @param authorities 用户权限
* @return JWT
*/
public String createJWT(Boolean rememberMe, Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) {
Date now = new Date();
JwtBuilder builder = Jwts.builder()
.setId(id.toString())
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, jwtConfig.getKey())
.claim("roles", roles)
.claim("authorities", authorities);
// 设置过期时间
Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
if (ttl > 0) {
builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));
}
String jwt = builder.compact();
// 将生成的JWT保存至Redis
stringRedisTemplate.opsForValue()
.set(Consts.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, TimeUnit.MILLISECONDS);
return jwt;
}
/**
* 创建JWT
*
* @param userPrincipal 用户认证信息
* @return JWT
*/
public String createJWT(UserPrincipal userPrincipal) {
return createJWT(userPrincipal.getRememberMe(), userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities());
}
/**
* 创建JWT
*
* @param authentication 用户认证信息
* @param rememberMe 记住我
* @return JWT
*/
public String createJWT(Authentication authentication, Boolean rememberMe) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities());
}
/**
* 解析JWT
*
* @param jwt JWT
* @return {@link Claims}
*/
public Claims parseJWT(String jwt) {
try {
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getKey())
.parseClaimsJws(jwt)
.getBody();
String username = claims.getSubject();
String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username;
// 校验redis中的JWT是否存在
Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);
if (Objects.isNull(expire) || expire <= 0) {
throw new SecurityException(ErrorCodeEnum.TOKEN_EXPIRED);
}
// 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期
String redisToken = stringRedisTemplate.opsForValue()
.get(redisKey);
if (!StrUtil.equals(jwt, redisToken)) {
throw new SecurityException(ErrorCodeEnum.TOKEN_OUT_OF_CTRL);
}
return claims;
} catch (ExpiredJwtException e) {
log.error("Token 已过期");
throw new SecurityException(ErrorCodeEnum.TOKEN_EXPIRED);
} catch (UnsupportedJwtException e) {
log.error("不支持的 Token");
throw new SecurityException(ErrorCodeEnum.TOKEN_PARSE_ERROR);
} catch (MalformedJwtException e) {
log.error("Token 无效");
throw new SecurityException(ErrorCodeEnum.TOKEN_PARSE_ERROR);
} catch (SignatureException e) {
log.error("无效的 Token 签名");
throw new SecurityException(ErrorCodeEnum.TOKEN_PARSE_ERROR);
} catch (IllegalArgumentException e) {
log.error("Token 参数不存在");
throw new SecurityException(ErrorCodeEnum.TOKEN_PARSE_ERROR);
}
}
/**
* 设置JWT过期
*
* @param request 请求
*/
public void invalidateJWT(HttpServletRequest request) {
String jwt = getJwtFromRequest(request);
String username = getUsernameFromJWT(jwt);
// 从redis中清除JWT
stringRedisTemplate.delete(Consts.REDIS_JWT_KEY_PREFIX + username);
}
/**
* 根据 jwt 获取用户名
*
* @param jwt JWT
* @return 用户名
*/
public String getUsernameFromJWT(String jwt) {
Claims claims = parseJWT(jwt);
return claims.getSubject();
}
/**
* 从 request 的 header 中获取 JWT
*
* @param request 请求
* @return JWT
*/
public String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Security 登录过滤器
UsernameLoginAuthenticationFilter 拦截登录请求
@Slf4j
public class UsernameLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public UsernameLoginAuthenticationFilter() {
super(new AntPathRequestMatcher("/api/auth/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
LoginAuthenticationToken token = new LoginAuthenticationToken(getRequest(request));
return this.getAuthenticationManager().authenticate(token);
}
private UserPrincipal getRequest(HttpServletRequest request) throws IOException{
String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
String username = null, password = null;
Boolean rememberMe = false;
if(StringUtils.hasText(body)) {
JSONObject jsonObj = JSON.parseObject(body);
username = jsonObj.getString("username");
password = jsonObj.getString("password");
rememberMe = jsonObj.getBoolean("rememberMe");
}
if (username == null){
username = "";
throw new AuthenticationServiceException("账号或密码不能为空");
}
if (password == null){
password = "";
}
if (rememberMe == null){
rememberMe = false;
}
username = username.trim();
UserPrincipal user = new UserPrincipal();
user.setUsername(username);
user.setPassword(password);
user.setRememberMe(rememberMe);
return user;
}
}
登录自定义处理 UsernameLoginAuthenticationProvider
@Setter
@Slf4j
public class UsernameLoginAuthenticationProvider implements AuthenticationProvider {
private UserLoginService userLoginService;
private BCryptPasswordEncoder encoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided");
log.debug("Enter UsernameLoginAuthenticationProvider...");
UserPrincipal req = (UserPrincipal) authentication.getPrincipal();
UserPrincipal user = userLoginService.loadUserByUsername(req.getUsername());
//校验密码
if(!encoder.matches(req.getPassword(),user.getPassword())){
throw new BadCredentialsException(ErrorCodeEnum.USERNAME_PASSWORD_ERROR.msg());
}
return new LoginAuthenticationToken(user,user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return (LoginAuthenticationToken.class.isAssignableFrom(authentication));
}
}
验证异常处理 LoginAuthenticationFailureHandler
@Component
@Slf4j
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.info("登录失败");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter()
.write(JSONUtil.toJsonStr(new JSONObject(ResponseWrapperMapper.wrap(e), false)));
}
}
验证成功 LoginAuthenticationSuccessHandler
@Component
@Slf4j
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
Assert.notNull(authentication, "No authentication data provided");
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
//校验成功
ResponseUtil.renderJson(httpServletResponse, ErrorCodeEnum.SUCCESS, new JwtResponse(jwtUtil.createJWT(user)));
}
}
UserLoginServiceImpl
实现 自定义UserLoginService接口,主要功能:根据用户名查询用户信息
/**
* @author : Lison
* @Date: 2019/10/28 16:24
* @Description: 自定义UserDetails查询
*/
@Service
public class UserLoginServiceImpl implements UserLoginService {
@Autowired
private PermissionRepository permissionRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Override
public UserPrincipal loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsernameOrEmailOrPhone(s, s, s)
.orElseThrow(() -> new UsernameNotFoundException("未找到用户信息 : " + s));
List<Role> roles = roleRepository.selectByUserId(user.getId());
List<Long> roleIds = roles.stream()
.map(Role::getId)
.collect(Collectors.toList());
List<Permission> permissions = permissionRepository.selectByRoleIdList(roleIds);
return UserPrincipal.create(user, roles, permissions,false);
}
jwt 过滤器
/**
* @author : Lison
* @Date: 2019/10/28 16:19
* @Description: Jwt 认证过滤器
*/
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserLoginService userLoginService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomConfig customConfig;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (checkIgnores(request)) {
filterChain.doFilter(request, response);
return;
}
String jwt = jwtUtil.getJwtFromRequest(request);
if (StrUtil.isNotBlank(jwt)) {
try {
String username = jwtUtil.getUsernameFromJWT(jwt);
//后期可使用缓存
UserPrincipal userDetails = userLoginService.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, ErrorCodeEnum.UNAUTHORIZED, null);
}
}
/**
* 请求是否不需要进行权限拦截
*
* @param request 当前请求
* @return true - 忽略,false - 不忽略
*/
private boolean checkIgnores(HttpServletRequest request) {
String method = request.getMethod();
HttpMethod httpMethod = HttpMethod.resolve(method);
if (ObjectUtil.isNull(httpMethod)) {
httpMethod = HttpMethod.GET;
}
Set<String> ignores = Sets.newHashSet();
switch (httpMethod) {
case GET:
ignores.addAll(customConfig.getIgnores()
.getGet());
break;
case PUT:
ignores.addAll(customConfig.getIgnores()
.getPut());
break;
case HEAD:
ignores.addAll(customConfig.getIgnores()
.getHead());
break;
case POST:
ignores.addAll(customConfig.getIgnores()
.getPost());
break;
case PATCH:
ignores.addAll(customConfig.getIgnores()
.getPatch());
break;
case TRACE:
ignores.addAll(customConfig.getIgnores()
.getTrace());
break;
case DELETE:
ignores.addAll(customConfig.getIgnores()
.getDelete());
break;
case OPTIONS:
ignores.addAll(customConfig.getIgnores()
.getOptions());
break;
default:
break;
}
ignores.addAll(customConfig.getIgnores()
.getPattern());
if (CollUtil.isNotEmpty(ignores)) {
for (String ignore : ignores) {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(ignore, method);
if (matcher.matches(request)) {
return true;
}
}
}
return false;
}
}
RbacAuthorityService.java
路由动态鉴权类,主要功能:
**
* @author : Lison
* @Date: 2019/10/28 17:05
* @Description: 动态路由认证
*/
@Component
public class RbacAuthorityService {
@Autowired
private RoleRepository roleRepository;
@Autowired
private PermissionRepository permissionRepository;
@Autowired
private RequestMappingHandlerMapping mapping;
private UrlPathHelper urlPathHelper;
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//取消校验是否为系统内部映射
// checkRequest(request);
Object userInfo = authentication.getPrincipal();
boolean hasPermission = false;
if (userInfo instanceof UserDetails) {
UserPrincipal principal = (UserPrincipal) userInfo;
Long userId = principal.getId();
List<Role> roles = roleRepository.selectByUserId(userId);
List<Long> roleIds = roles.stream()
.map(Role::getId)
.collect(Collectors.toList());
List<Permission> permissions = permissionRepository.selectByRoleIdList(roleIds);
//获取资源,前后端分离,所以过滤页面权限,只保留按钮权限
List<Permission> btnPerms = permissions.stream()
// 过滤页面权限
.filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON))
// 过滤 URL 为空
.filter(permission -> StrUtil.isNotBlank(permission.getUrl()))
// 过滤 METHOD 为空
.filter(permission -> StrUtil.isNotBlank(permission.getMethod()))
.collect(Collectors.toList());
for (Permission btnPerm : btnPerms) {
AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod());
if (antPathMatcher.matches(request)) {
hasPermission = true;
break;
}
}
return hasPermission;
} else {
return false;
}
}
/**
* 校验请求是否存在
*
* @param request 请求
*/
private void checkRequest(HttpServletRequest request) {
// 获取当前 request 的方法
String currentMethod = request.getMethod();
Multimap<String, String> urlMapping = allUrlMapping();
for (String uri : urlMapping.keySet()) {
// 通过 AntPathRequestMatcher 匹配 url
// 可以通过 2 种方式创建 AntPathRequestMatcher
// 1:new AntPathRequestMatcher(uri,method) 这种方式可以直接判断方法是否匹配,因为这里我们把 方法不匹配 自定义抛出,所以,我们使用第2种方式创建
// 2:new AntPathRequestMatcher(uri) 这种方式不校验请求方法,只校验请求路径
AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri);
if (antPathMatcher.matches(request)) {
if (!urlMapping.get(uri)
.contains(currentMethod)) {
throw new SecurityException(ErrorCodeEnum.HTTP_BAD_METHOD);
} else {
return;
}
}
}
throw new SecurityException(ErrorCodeEnum.REQUEST_NOT_FOUND);
}
/**
* 获取 所有URL Mapping,返回格式为{"/test":["GET","POST"],"/sys":["GET","DELETE"]}
*
* @return {@link ArrayListMultimap} 格式的 URL Mapping
*/
private Multimap<String, String> allUrlMapping() {
Multimap<String, String> urlMapping = ArrayListMultimap.create();
// 获取url与类和方法的对应信息
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
handlerMethods.forEach((k, v) -> {
// 获取当前 key 下的获取所有URL
Set<String> url = k.getPatternsCondition()
.getPatterns();
RequestMethodsRequestCondition method = k.getMethodsCondition();
// 为每个URL添加所有的请求方法
url.forEach(s -> urlMapping.putAll(s, method.getMethods()
.stream()
.map(Enum::toString)
.collect(Collectors.toList())));
});
return urlMapping;
}
private String getRequestPath(HttpServletRequest request) {
if (this.urlPathHelper != null) {
return this.urlPathHelper.getPathWithinApplication(request);
} else {
String url = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
url = StringUtils.hasLength(url) ? url + pathInfo : pathInfo;
}
return url;
}
}
}
其余代码详细见集成项目 项目GitHub地址