springboot 常用的验证框架分析 -shiro/springsecurity
一 常用的认证鉴权框架
关于认证和鉴权的框架,在springboot中使用比较多的比如shiro,spring security,soToken这些。从设计上,这些框架的底层逻辑其实大同小异。
整体上来说: 对于保护性的安全资源,用户需要先通过认证,才能获取授权访问,所以通过理解,很容易思考到,所有的权限管理框架。至少包含两个东西。
安全管理器(SecurityManager):用户如何认证(CustomRealm),权限资源的授权策略(ShiroFilterFactoryBean),用户session管理(SessionManager),提高权限验证的缓存管理器(CacheManager)
权限标识(AuthorizingRealm): 包含认证管理器(Authenticator)和授权管理器(Authorizer)
二 对shiro的框架的分析
下面是shiro的鉴权框架结构:
对于上面复杂的架构,我们只需要记住以下几个核心模块:
1.Subject
主体,你可以理解为访问系统的用户。
2.SecurityManager
安全管理器。用户进行认证和授权都是通过 securityManager 进行,你可以理解为 shiro 的老大。
3.authenticator
认证器,用户通过 authenticator 进行认证。
4.authorizer
授权器,用户通过 authorizer 进行授权。
5.realm
领域,相当于数据源。
在 realm 中,我们通过查询数据库的信息,然后对用户进行认证和授权。
所以 authenticator 和 authorizer 其实是调用了 realm 中 认证和授权的方法。
6.cryptography
密码管理。shiro 提供了一套加密和解密的组
三 对 springsecurity 框架分析
实现步骤:
- 构建一个自定义的service实现类,实现SpringSecurity的UserDetailService接口中的loadUserByUsername方法。
- 创建一个自定义的登录类,实现login方法,在login方法中调用 authenticationManager.authenticate(authenticationToken)方法。此方法会调用UserDetailService中的loadUserByUsername方法
- 创建一个loginUser类,继承自 UserDetails 来代表自定义的权限标识载体
/***登录部分最核心的部分逻辑***/ try { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); AuthenticationContextHolder.setContext(authenticationToken); // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager.authenticate(authenticationToken); }
authenticationManager.authenticate()调用 loadUserByUsername 方法进行用户身份证的验证和授权。
写一个用户的user类来代表登录的信息,注意:UserDetails 为 springSecurity中的权限标识
@ToString public class LoginUser implements UserDetails { private static final long serialVersionUID = 1L; /** * 用户ID */ private Long userId; /** * 部门ID */ private Long deptId; /** * 用户唯一标识 */ private String token; /** * 登录时间 */ private Long loginTime; /** * 过期时间 */ private Long expireTime; /** * 登录IP地址 */ private String ipaddr; /** * 登录地点 */ private String loginLocation; /** * 浏览器类型 */ private String browser; /** * 操作系统 */ private String os; /** * 权限列表 */ private Set<String> permissions; /** * 用户信息 */ private SysUser user; public LoginUser() { } public LoginUser(SysUser user, Set<String> permissions) { this.user = user; this.permissions = permissions; } public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions) { this.userId = userId; this.deptId = deptId; this.user = user; this.permissions = permissions; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getDeptId() { return deptId; } public void setDeptId(Long deptId) { this.deptId = deptId; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } @JSONField(serialize = false) @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } public Integer getUserDataPermissions() { return user.getDataPermissions(); } public String getUserCompanyId() { return user.getCompanyId(); } public Integer getCoId(){ return user.getCoId(); } public void setCoId(Integer coId){} /** * 账户是否未过期,过期无法验证 */ @JSONField(serialize = false) @Override public boolean isAccountNonExpired() { return true; } /** * 指定用户是否解锁,锁定的用户无法进行身份验证 * * @return */ @JSONField(serialize = false) @Override public boolean isAccountNonLocked() { return true; } /** * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 * * @return */ @JSONField(serialize = false) @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 ,禁用的用户不能身份验证 * * @return */ @JSONField(serialize = false) @Override public boolean isEnabled() { return true; } public Long getLoginTime() { return loginTime; } public void setLoginTime(Long loginTime) { this.loginTime = loginTime; } public String getIpaddr() { return ipaddr; } public void setIpaddr(String ipaddr) { this.ipaddr = ipaddr; } public String getLoginLocation() { return loginLocation; } public void setLoginLocation(String loginLocation) { this.loginLocation = loginLocation; } public String getBrowser() { return browser; } public void setBrowser(String browser) { this.browser = browser; } public String getOs() { return os; } public void setOs(String os) { this.os = os; } public Long getExpireTime() { return expireTime; } public void setExpireTime(Long expireTime) { this.expireTime = expireTime; } public Set<String> getPermissions() { return permissions; } public void setPermissions(Set<String> permissions) { this.permissions = permissions; } public SysUser getUser() { return user; } public void setUser(SysUser user) { this.user = user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } }
loadUserByUsername() 方法内部实现。
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPasswordService passwordService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException(MessageUtils.message("user.not.exists"));
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException(MessageUtils.message("user.password.delete"));
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException(MessageUtils.message("user.blocked"));
}
passwordService.validate(user);
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
扩展自己的jwt token信息
通过分析springSecurity的权限认证流程,可以看出是基于过滤器链的,如果需要添加自定义的token验证,可以考虑在
添加自定义的过滤器最简单直接的方法是 扩展 WebSecurityConfigurerAdapter
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; /** * 允许匿名访问的地址 */ @Autowired private PermitAllUrlProperties permitAllUrl; /** * 解决 无法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { // 注解标记允许匿名访问的url ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 禁用HTTP响应标头 .headers().cacheControl().disable().and() // 认证失败处理类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/register", "/captchaImage","/plat","/common").permitAll() //对api路径下的请求暴露 .antMatchers("/api/**").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() //对外暴露 接口逻辑 标记/plat .antMatchers( "/plat/**").permitAll() .antMatchers( "/pj/import").permitAll() .antMatchers( "/pj/excel/import").permitAll() .antMatchers( "/pj/template/download").permitAll() .antMatchers( "/bus/platOrderPre/reSend").permitAll() .antMatchers( "/bus/platOrderPre/batchReSend").permitAll() .antMatchers( "/bus/platOrderPre/queryListReSend").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() .antMatchers("/patrol/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); // 添加Logout filter httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS filter httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); } /** * 强散列哈希加密实现 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 身份认证接口 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } }
自定义的 JwtAuthenticationTokenFilter 进行token验证
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private TokenService tokenService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { LoginUser loginUser = tokenService.getLoginUser(request); if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) { tokenService.verifyToken(loginUser); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); } } @Component public class TokenService { private static final Logger log = LoggerFactory.getLogger(TokenService.class); // 令牌自定义标识 @Value("${token.header}") private String header; // 令牌秘钥 @Value("${token.secret}") private String secret; // 令牌有效期(默认30分钟) @Value("${token.expireTime}") private int expireTime; protected static final long MILLIS_SECOND = 1000; protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; @Autowired private RedisCache redisCache; /** * 获取用户身份信息 * * @return 用户信息 */ public LoginUser getLoginUser(HttpServletRequest request) { // 获取请求携带的令牌 String token = getToken(request); if (StringUtils.isNotEmpty(token)) { try { Claims claims = parseToken(token); // 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = getTokenKey(uuid); LoginUser user = redisCache.getCacheObject(userKey); return user; } catch (Exception e) { log.error("获取用户信息异常'{}'", e.getMessage()); } } return null; } /** * 设置用户身份信息 */ public void setLoginUser(LoginUser loginUser) { if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) { refreshToken(loginUser); } } /** * 删除用户身份信息 */ public void delLoginUser(String token) { if (StringUtils.isNotEmpty(token)) { String userKey = getTokenKey(token); redisCache.deleteObject(userKey); } } /** * 创建令牌 * * @param loginUser 用户信息 * @return 令牌 */ public String createToken(LoginUser loginUser) { String token = IdUtils.fastUUID(); loginUser.setToken(token); setUserAgent(loginUser); refreshToken(loginUser); Map<String, Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); return createToken(claims); } /** * 验证令牌有效期,相差不足20分钟,自动刷新缓存 * * @param loginUser * @return 令牌 */ public void verifyToken(LoginUser loginUser) { long expireTime = loginUser.getExpireTime(); long currentTime = System.currentTimeMillis(); if (expireTime - currentTime <= MILLIS_MINUTE_TEN) { refreshToken(loginUser); } } /** * 刷新令牌有效期 * * @param loginUser 登录信息 */ public void refreshToken(LoginUser loginUser) { loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); // 根据uuid将loginUser缓存 String userKey = getTokenKey(loginUser.getToken()); redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); } /** * 设置用户代理信息 * * @param loginUser 登录信息 */ public void setUserAgent(LoginUser loginUser) { UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); String ip = IpUtils.getIpAddr(); loginUser.setIpaddr(ip); loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); loginUser.setBrowser(userAgent.getBrowser().getName()); loginUser.setOs(userAgent.getOperatingSystem().getName()); } /** * 从数据声明生成令牌 * * @param claims 数据声明 * @return 令牌 */ private String createToken(Map<String, Object> claims) { String token = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret).compact(); return token; } /** * 从令牌中获取数据声明 * * @param token 令牌 * @return 数据声明 */ private Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } /** * 从令牌中获取用户名 * * @param token 令牌 * @return 用户名 */ public String getUsernameFromToken(String token) { Claims claims = parseToken(token); return claims.getSubject(); } /** * 获取请求token * * @param request * @return token */ private String getToken(HttpServletRequest request) { String token = request.getHeader(header); if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) { token = token.replace(Constants.TOKEN_PREFIX, ""); } return token; } private String getTokenKey(String uuid) { return CacheConstants.LOGIN_TOKEN_KEY + uuid; } }