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:
SecurityContextPersistenceFilter,用于将SecurityContext放入Session的FilterUsernamePasswordAuthenticationFilter, 登录认证的Filter,类似的还有CasAuthenticationFilter,BasicAuthenticationFilter等等。在这些Filter中生成用于认证的token,提交到AuthenticationManager,如果认证失败会直接返回。RememberMeAuthenticationFilter,通过cookie来实现remember me功能的FilterAnonymousAuthenticationFilter,如果一个请求在到达这个filter之前SecurityContext没有初始化,则这个filter会默认生成一个匿名SecurityContext。这在支持匿名用户的系统中非常有用。ExceptionTranslationFilter,捕获所有Spring Security抛出的异常,并决定处理方式FilterSecurityInterceptor, 权限校验的拦截器,访问的url权限不足时会抛出异常pom.xml 添加配置
<jjwt.veersion>0.9.1</jjwt.veersion> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <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
路由动态鉴权类,主要功能:
校验请求的合法性,排除404和405这两种异常请求根据当前请求路径与该用户可访问的资源做匹配,通过则可以访问,否则,不允许访问 ** * @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地址