总体思路
大体的设计思路和之前比较一致,只是在配置方面做了调整,重新梳理如下:
- 构建一个特定的Token类,例如
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呢结果已经被人修复好了。。。。。
附用到的一些配置类
Redis
Redis操作工具类
@Component
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public class RedisCacheUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public Boolean setExpire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获取TTL,单位秒
*
* @param key Redis键
* @return TTL
*/
public Long getExpire(final String key) {
return redisTemplate.getExpire(key);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setObject(String key, T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setObject(String key, T value, long timeout, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 获得缓存的基本对象
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getObject(String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key 缓存键值
*/
public Boolean deleteObject(String key) {
return redisTemplate.delete(key);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setList(String key, List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getList(String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Map
*
* @param key 缓存键值
* @param dataMap 要缓存的Map
*/
public <T> void setMap(String key, Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key 缓存键值
* @return 缓存的Map
*/
public <T> Map<String, T> getMap(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key 缓存键值
* @param hKey Hash键
* @param value 值
*/
public <T> void setMapValue(String key, String hKey, T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key 缓存键值
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getMapValue(String key, String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 删除Hash中的数据
*
* @param key 缓存键值
* @param hKey Hash键
*/
public void deleteMapValue(String key, String hKey) {
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hKey);
}
}
RedisTemplate配置类
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new RedisFastJsonSerializer<>(Object.class));
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new RedisFastJsonSerializer<>(Object.class));
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
return template;
}
}
Redis使用FastJson2序列化配置类
public class RedisFastJsonSerializer<T> implements RedisSerializer<T> {
private final Class<T> clazz;
public RedisFastJsonSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(StandardCharsets.UTF_8);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length == 0) {
return null;
}
String str = new String(bytes, StandardCharsets.UTF_8);
return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
}
}
JWT
此jwt使用的是以下版本的类库,注意不同实现库的加解密方式可能不同
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.2</version>
</dependency>
public class JwtTokenUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String JWT_KEY = "替换成你自己的key";
private static final String ISSUER = "替换成你自己的署名者";
/**
* 生成JWT Token
*
* @param paramMap 需要写在token中的参数
* @return token
*/
public static String createJwtToken(Map<String, Object> paramMap) {
Algorithm algorithm = Algorithm.HMAC256(JWT_KEY);
return JWT.create().withIssuer(ISSUER).withClaim("user", paramMap).withIssuedAt(new Date()).sign(algorithm);
}
/**
* token校验
*
* @param token token
* @return true or false
*/
public static boolean checkToken(String token) {
Algorithm algorithm = Algorithm.HMAC256(JWT_KEY);
JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
try {
verifier.verify(token);
return true;
} catch (Exception e) {
logger.error("token校验失败: " + e.getMessage(), e);
return false;
}
}
/**
* 获取token
*
* @param request HttpServletRequest
* @return token
*/
public static String getToken(HttpServletRequest request) {
String token = request.getHeader(TokenConstant.TOKEN_HEADER);
return Objects.isNull(token) || "null".equalsIgnoreCase(token) || StringUtils.isBlank(token) ? null : token;
}
}