上篇文章已经大致把SpringSecurity
的设计思路捋出来了,这次就开始着手改造一下。
大体的设计思路和之前比较一致,只是在配置方面做了调整,重新梳理如下:
PasswordAuthenticationToken
,这个类需要继承AbstractAuthenticationToken
,在需要做认证的地方把他new出来;PasswordAuthenticationProvider
类,实现AuthenticationProvider
接口,并重写其中的authenticate()
与supports()
方法;PasswordAuthenticationFilter
类继承自AbstractAuthenticationProcessingFilter
并重写其中的attemptAuthentication()
方法,同时重写AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher,AuthenticationManager authenticationManager)
方法用于注入AuthenticationManager
;PasswordAuthenticationConfigurer
类,继承自AbstractHttpConfigurer
并重写configure
方法,他可是提供了一个非常宝贵的HttpSecurity
入参。思路清楚了就直接上代码吧,关键位置都做了注释,一遍看不懂就多看几遍。先看下改造后的整体结构。
先来PasswordAuthenticationToken
/**
* 基于用户名(手机号)、密码、验证码登录的认证实体
*/
public class PasswordAuthenticationToken extends AbstractAuthenticationToken {
private final String loginId;
private final String captchaId;
private final String captchaValue;
private final LoginUserPojo principal;
private final String credentials;
/**
* 登录验证
*
* @param loginId 用户名或手机号
* @param credentials MD5+SM3密码
* @param captchaId 图形验证码id
* @param captchaValue 输入的图形验证码值
*/
public PasswordAuthenticationToken(String loginId, String credentials, String captchaId, String captchaValue) {
super(null);
this.loginId = loginId;
this.credentials = credentials;
this.captchaId = captchaId;
this.captchaValue = captchaValue;
this.principal = null;
this.setAuthenticated(false);
}
/**
* 授权信息
*
* @param principal LoginUserPojo
* @param credentials token
* @param authorities 角色清单
*/
public PasswordAuthenticationToken(LoginUserPojo principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
this.loginId = null;
this.captchaId = null;
this.captchaValue = null;
this.setAuthenticated(true);
}
public String getLoginId() {
return loginId;
}
public String getCaptchaId() {
return captchaId;
}
public String getCaptchaValue() {
return captchaValue;
}
@Override
public LoginUserPojo getPrincipal() {
return principal;
}
@Override
public String getCredentials() {
return credentials;
}
}
PasswordAuthenticationProvider
/**
* 基于用户名(手机号)、密码、验证码的认证处理器
*/
public class PasswordAuthenticationProvider implements AuthenticationProvider {
private static final String IMG_CAPTCHA_REDIS_PREFIX = "SK:CAPTCHA:IMG:";
private final UserDetailServiceImpl userDetailService;
private final RedisCacheUtil redisCacheUtil;
public PasswordAuthenticationProvider(UserDetailServiceImpl userDetailService, RedisCacheUtil redisCacheUtil) {
this.userDetailService = userDetailService;
this.redisCacheUtil = redisCacheUtil;
}
/**
* 验证主逻辑
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PasswordAuthenticationToken authToken = (PasswordAuthenticationToken) authentication;
// 验证码校验
if (!checkImgCaptcha(authToken.getCaptchaId(), authToken.getCaptchaValue())) {
throw new BadCaptchaException("验证码有误或已过期,请重新输入");
}
// 密码校验
LoginUserPojo userDetails = (LoginUserPojo) userDetailService.loadUserByUsername(authToken.getLoginId());
if (!new BCryptPasswordEncoder().matches(authToken.getCredentials(), userDetails.getPassword())) {
throw new BadCredentialsException("用户名或密码错误,请重新输入");
}
// 用户状态校验
if (!userDetails.isEnabled() || !userDetails.isAccountNonLocked() || !userDetails.isAccountNonExpired()) {
throw new LockedException("用户已禁用,请联系管理员启用");
}
return new PasswordAuthenticationToken(userDetails, authToken.getCredentials(), userDetails.getAuthorities());
}
/**
* 当类型为PasswordAuthenticationToken的认证实体进入时才走此Provider
*/
@Override
public boolean supports(Class<?> authentication) {
return PasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* 校验验证码正确与否,验证完成后删除当前码值
*
* @param id 验证码对应的id
* @param value 用户输入的验证码结果
* @return true or false
*/
private boolean checkImgCaptcha(String id, String value) {
if (StringUtils.isBlank(id) || StringUtils.isBlank(value)) {
return false;
}
CaptchaCodePojo captchaCode = redisCacheUtil.getObject(IMG_CAPTCHA_REDIS_PREFIX + id);
redisCacheUtil.deleteObject(IMG_CAPTCHA_REDIS_PREFIX + id);
return !Objects.isNull(captchaCode) && value.equals(captchaCode.getResult());
}
}
PasswordAuthenticationConfigurer
/**
* 基于用户名(手机号)、密码、验证码的登录拦截器配置类
*/
public class PasswordAuthenticationConfigurer extends AbstractHttpConfigurer<PasswordAuthenticationConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) {
// 拦截 POST /login 请求
RequestMatcher matcher = new AntPathRequestMatcher("/login", "POST", true);
UserDetailServiceImpl userDetailService = builder.getSharedObject(ApplicationContext.class).getBean(UserDetailServiceImpl.class);
RedisCacheUtil redisCacheUtil = builder.getSharedObject(ApplicationContext.class).getBean(RedisCacheUtil.class);
AuthenticationManager localAuthManager = builder.getSharedObject(AuthenticationManager.class);
PasswordAuthenticationFilter filter = new PasswordAuthenticationFilter(matcher, localAuthManager);
filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(userDetailService));
filter.setAuthenticationFailureHandler(new LoginFailHandler());
// 务必注意这里与配置类中声明的先后顺序
builder.authenticationProvider(new PasswordAuthenticationProvider(userDetailService, redisCacheUtil))
.addFilterBefore(filter, AuthenticationTokenFilter.class);
}
}
PasswordAuthenticationFilter
/**
* 用户名密码登录拦截器
*/
public class PasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public PasswordAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) {
super(requiresAuthenticationRequestMatcher, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
JSONObject params = HttpRequestUtil.getBodyJson(request);
Authentication authentication = new PasswordAuthenticationToken(
params.getString("loginKey"), params.getString("password"), params.getString("id"), params.getString("value")
);
return this.getAuthenticationManager().authenticate(authentication);
}
}
Token拦截器配置类TokenAuthenticationConfigurer
/**
* Token拦截器配置类
*/
public class TokenAuthenticationConfigurer extends AbstractHttpConfigurer<TokenAuthenticationConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) {
RedisCacheUtil redisCacheUtil = builder.getSharedObject(ApplicationContext.class).getBean(RedisCacheUtil.class);
builder.addFilterBefore(new AuthenticationTokenFilter(redisCacheUtil), UsernamePasswordAuthenticationFilter.class);
}
}
Token认证拦截器AuthenticationTokenFilter
/**
* Token认证拦截器
*/
public class AuthenticationTokenFilter extends OncePerRequestFilter {
private final RedisCacheUtil redisCacheUtil;
public AuthenticationTokenFilter(RedisCacheUtil redisCacheUtil) {
this.redisCacheUtil = redisCacheUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = JwtTokenUtil.getToken(request);
if (Objects.nonNull(token) && JwtTokenUtil.checkToken(token) && redisCacheUtil.hasKey(TokenConstant.TOKEN_REDIS_PREFIX + token)) {
// 从redis中获取数据
LoginUserPojo userPojo = redisCacheUtil.getObject(TokenConstant.TOKEN_REDIS_PREFIX + token);
// 写入上下文
PasswordAuthenticationToken authenticationToken = new PasswordAuthenticationToken(userPojo, token, userPojo.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 刷新ttl
redisCacheUtil.setExpire(TokenConstant.TOKEN_REDIS_PREFIX + token, TokenConstant.TOKEN_EXPIRE_TIME, TokenConstant.TOKEN_EXPIRE_TIME_UNIT);
}
filterChain.doFilter(request, response);
}
}
LoginFailHandler
/**
* 密码认证失败处理器
*/
public class LoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
JSONObject res = new JSONObject();
res.put("success", false);
res.put("msg", e.getMessage());
response.setStatus(HttpStatus.SC_FORBIDDEN);
response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().print(res.toJSONString());
}
}
LoginSuccessHandler
/**
* 登录成功后处理器
*/
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private final UserDetailServiceImpl userDetailsService;
public LoginSuccessHandler(UserDetailServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
LoginUserPojo loginUserPojo = (LoginUserPojo) authentication.getPrincipal();
// 更新登陆时间
userDetailsService.updateLoginTime(loginUserPojo.getUserId());
// 构建token并缓存
String token = userDetailsService.buildToken(loginUserPojo);
JSONObject res = new JSONObject();
res.put("success", true);
res.put("msg", "OK");
res.put("data", token);
response.setStatus(HttpStatus.SC_OK);
response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().write(res.toString());
}
}
UserDetailServiceImpl
/**
* SpringSecurity登录处理类
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private RedisCacheUtil redisCacheUtil;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Map<String, Object> userData = sysUserMapper.getByUserNameOrUserPhone(username);
if (MapUtils.isEmpty(userData)) {
throw new UsernameNotFoundException("用户名或密码错误,请重新输入");
}
// 封装基础数据
LoginUserPojo userPojo = new LoginUserPojo();
userPojo.setUserId(MapUtils.getString(userData, "user_id"));
userPojo.setUserName(MapUtils.getString(userData, "user_name"));
userPojo.setUserPhone(MapUtils.getString(userData, "user_phone"));
userPojo.setNickName(MapUtils.getString(userData, "nick_name"));
userPojo.setPassword(MapUtils.getString(userData, "password"));
userPojo.setUserStatus(MapUtils.getIntValue(userData, "user_status"));
userPojo.setLastLogin(MapUtils.getString(userData, "last_login"));
userPojo.setLastUpdatePassword(MapUtils.getString(userData, "last_update_password"));
userPojo.setCreateUser(MapUtils.getString(userData, "create_user"));
userPojo.setCreateTime(MapUtils.getString(userData, "create_time"));
userPojo.setUpdateUser(MapUtils.getString(userData, "update_user"));
userPojo.setUpdateTime(MapUtils.getString(userData, "update_time"));
// 封装角色信息
if (StringUtils.isNotBlank(MapUtils.getString(userData, "role_code"))) {
List<UserGrantedAuthority> grantedAuthorityList = new ArrayList<>();
String[] roleCodes = MapUtils.getString(userData, "role_code").split(",");
String[] roleNames = MapUtils.getString(userData, "role_name").split(",");
String[] roleTypes = MapUtils.getString(userData, "role_type").split(",");
for (int i = 0; i < roleCodes.length; i++) {
grantedAuthorityList.add(new UserGrantedAuthority(roleCodes[i], roleNames[i], Integer.valueOf(roleTypes[i])));
}
userPojo.setAuthorities(grantedAuthorityList);
}
return userPojo;
}
/**
* 更新用户登录时间
*
* @param userId 用户ID
*/
public void updateLoginTime(String userId) {
SysUserEntity userEntity = new SysUserEntity();
userEntity.setUserId(userId);
userEntity.setLastLogin(DateTimeUtil.getCurrentDate("yyyy-MM-dd HH:mm:ss"));
sysUserMapper.updateByUserId(userEntity);
}
/**
* 根据用户信息构造token并写入redis
*
* @param loginUserPojo LoginUserPojo
* @return token
*/
public String buildToken(LoginUserPojo loginUserPojo) {
JSONObject user = new JSONObject();
user.put("userId", loginUserPojo.getUserId());
user.put("userName", loginUserPojo.getUserName());
user.put("roleCode", loginUserPojo.getAuthorities().stream().map(UserGrantedAuthority::getRoleCode).collect(Collectors.joining(",")));
// 生成token
String token = JwtTokenUtil.createJwtToken(user);
redisCacheUtil.setObject(TokenConstant.TOKEN_REDIS_PREFIX + token, loginUserPojo, TokenConstant.TOKEN_EXPIRE_TIME, TokenConstant.TOKEN_EXPIRE_TIME_UNIT);
return token;
}
}
还有最后的Security配置类SpringSecurityConfig
/**
* SpringSecurity配置类
*/
@EnableWebSecurity
@PropertySource("classpath:authfilter.properties")
public class SpringSecurityConfig {
@Value("${exclude_urls}")
private String excludeUrls;
@Autowired
private AuthenticationLogoutHandler logoutHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf().disable()
.formLogin().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeHttpRequests().antMatchers(StringUtils.split(excludeUrls, ",")).permitAll()
.anyRequest().authenticated().and()
.exceptionHandling().authenticationEntryPoint(new AuthenticationFailHandler()).and()
.logout().logoutUrl("/logout").logoutSuccessHandler(logoutHandler).and()
// 务必注意这里的先后顺序,否则会报NULL异常
.apply(new TokenAuthenticationConfigurer()).and()
.apply(new PasswordAuthenticationConfigurer()).and()
.apply(new OAuthAuthenticationConfigurer()).and()
.build();
}
}
经过测试完美执行,自定义Provider可以仅在Local作用域中生效,不干扰全局AuthenticationManager
。
另外我想尽可能避免将所有类都标记为@Component
,所以部分业务依赖项我是通过上下文ApplicationContext
获取的,而这个实例正好也在httpSecurity
的sharedObject
中。
当我完成所有修改后已经过了三天多了,这段时间Security简直就是我的梦魇。
但我也和大多数人一样,看之前骂spring一个过滤器的事设计的这么复杂干什么,看完之后才发现给我上了一课:什么叫设计模式,什么叫开闭原则。跟各位写框架的大佬比,我还是太年轻了。
不过倒也发现了一些问题,比如在他推荐使用SecurityFilterChain
的时候,javadoc里面却仍然是传统方式的例子。满心欢喜的以为能水一个issue呢结果已经被人修复好了。。。。。
附用到的一些比较有价值的资料(还是推荐spring官方文档,要一字一句的读才能体会到设计者的思路):
一文搞懂SpringSecurity+JWT前后端分离
避坑指南(三):Spring Security Oauth2框架如何初始化AuthenticationManager
Spring Security 实战干货:你不了解的Spring Security架构
Spring Security 实战干货:WebSecurityConfigurerAdapter即将被移除
SpringSecurity之ProviderManager是如何产生的,如何与UsernamePasswordAuthenticationFilter光联在一起
Spring Security小教程 Vol 3. 身份验证的入口-AbstractAuthenticationProcessingFilter
Spring Security without the WebSecurityConfigurerAdapter
ProviderManager