发表于: 2020-01-12 17:50:36
2 1184
今天完成的事情:
1、整合 SpringSecurity
SpringSecurity 是一个框架,侧重于为 Java 应用程序提供身份验证和授权。SpringSecurity 安全性的真正强大之处在很容易自定义扩展满足定制需求
1)SpringSecurity 基本配置
引入相关依赖:
<!-- 实现对 Spring Security 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
完成 SpringSecurity 在 web 场景下的自定义配置:SecurityConfig
重写 WebSecurityConfigureAdapter 方法,实现自定义配置:configure(AuthencationManagerBuilder auth),实现 AuthencationManager r认证管理器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
Spring 内置了两种 UserDetailsManager 实现
基于内存 InMemoryUserDetailManager 和 基于 JDBC 的 JdbcUserDetailsManager,在实际项目中一般会使用自定义实现的 UserDetailsService 实现类,更灵活自由的实现用户信息的读取和认证
此外还调用了 passwordEncoder ,这里设置了 PasswordEncoder 对密码进行加密
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
然后重写 configure(HttpSecurity http)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
//基于 token,所以不需要 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers(HttpMethod.OPTIONS,"/**").permitAll()
.antMatchers(HttpMethod.GET,"/*.html","/**/*.html","/**/*.css","/**/*.js").permitAll(
.anyRequest().authenticated()
.and()
.headers().frameOptions().sameOrigin();
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
首先将 /auth 路径下的登录,注册添加为 permitAll 表示不需要认证就可以操作
其次一些静态资源文件如 html css js
剩下的访问路径都需要经过认证 设置为 anyRequest().authenticated()
还需要定义 AuthencationManager bean 实例,方便自己在程序中调用 authencationManager.authenticate 方法对用户名密码进行校验
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
2)SpringSecurity 认证相关处理类
自定义 UserDetails
JwtUser 实现 UserDetails 接口
public class JwtUser implements UserDetails{
@JsonIgnore
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*
* @return
*/
@JsonIgnore
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return
*/
@JsonIgnore
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return
*/
@JsonIgnore
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;}
}
自定义 UserDetailService
实现 SpringSecurity 的 UserDetailsService 接口,重写 loadUserByUsername
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private UserInfoService userInfoService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询指定用户名对应的 UserInfo
UserInfo userInfo = userInfoService.findUserInfoByAccount(username);
if (StringUtils.isNull(userInfo)) { // 用户不存在
throw new UsernameNotFoundException("用户:" + username + " 不存在");
}
// 创建 Spring Security UserDetails 用户明细
return createLoginUser(userInfo);
}
public UserDetails createLoginUser(UserInfo user) {
return new JwtUser(user,null);
}
}
UserService 是自己实现的类,负责去数据库中查询用户信息,最后生成 UserDetails 返回给 AuthenticationManager
@Override
public UserInfo findUserInfoByAccount(String phoneNum) {
UserInfo userInfo = new UserInfo();
userInfo.setPhoneNum(phoneNum);
return userInfoMapper.selectOne(userInfo);
}
那么负责从前端接收用户提交的用户名密码如下:
@Override
public UserInfo login(UserInfo userInfo) {
String userName = userInfo.getPhoneNum();
String password = userInfo.getPassword();
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(userName, password);
Authentication authentication;
try {
authentication = authenticationManager.authenticate(upToken);
} catch (Exception e) {
if(e instanceof BadCredentialsException){
throw new ServiceException(ServiceExceptionEnum.USER_NOT_MATCH);
}
else{
throw new ServiceException(ServiceExceptionEnum.USER_ACCOUNT_ERROR);
}
}
JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
userInfo.setToken(tokenService.createToken(jwtUser));
return userInfo;
}
将用户名密码生成 UsernamePasswordAuthenticationToken 这个 token 传入 authenticate() 方法中,
那么之前一步 AuthenticationManager 接收也从数据库中读取到的用户信息了,就可以与 token 中的密码凭证进行比对,然后如果不匹配则抛出 BadCredentials 异常,表示账号密码不匹配。如果校验通过则返回Authencation 其中包含了 JwtUser(UserDetails)的所有信息
2、基于 JWT 实现登录认证操作
1)登陆成功生成 Token
采用 JWT 生成 Token
public String createToken(JwtUser loginUser) {
// 设置 JWTUser 的用户唯一标识。注意,这里虽然变量名叫 token ,其实不是身份认证的 Token
String token = IdUtils.fastUUID();
loginUser.setToken(token);
// 记录缓存
refreshToken(loginUser);
// 生成 JWT 的 Token
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims) {
String originToken = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return originToken;
}
2)Redis 缓存 token
SpringBoot 对 Redis 的支持也很友好,实现 Redis 自动化配置
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory){
//创建 RedisTemplate 对象
RedisTemplate<Object,Object> template = new RedisTemplate<>();
//设置 RedisConnection 工厂
template.setConnectionFactory(factory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
//使用 String 序列化方式序列化 key
template.setKeySerializer(new StringRedisSerializer());
//使用 json 序列化方式(jason 库)序列化 value
template.setValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
这里调用 RedisTemplate 将生成的 Token 保存至 Redis
/**
* 刷新令牌有效期
*
* @param jwtUser 登录用户信息
*/
public void refreshToken(JwtUser jwtUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据 uuid 将 JwtUser 缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
3)自定义 JWTTokenFilter
这里是采用 Token 校验机制(跟传统的 Session Cookie 机制不一样),前端在提交请求的时候需要在 Http Header 中携带 Token
所以需要自定义一个 Filter 来对 Request 中的 Token 进行校验
JwtAuthencationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获得当前 JwtUserUser
JwtUser jwtUser = tokenService.getLoginUser(httpServletRequest);
// 如果存在 JwtUser ,并且未认证过
if (StringUtils.isNotNull(jwtUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
// 校验 Token 有效性
tokenService.verifyToken(jwtUser);
// 创建 UsernamePasswordAuthenticationToken 对象,注入到 SecurityContextHolder 中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtUser, null, jwtUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
// 继续过滤器
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
如果能够根据 Token 取出来用户信息,则证明认证通过,然后通过 SecurityContextHolder setAuthentication 来共享认证过的用户凭证。
将自己定义的 filter 加入到 SecurtiyConfig 配置中:
@Overrideprotected void configure(HttpSecurity http) throws Exception {
....//省略之前的配置
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
AuthenticationEntryPoint :负责处理验证不通过( 这里自定义的验证机制,如果SpringSecurity 中没有注入 aut),给前端返回信息,从而做进一步处理
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
int code = HttpStatus.UNAUTHORIZED.value();
String msg = "认证失败,请重新登录";
ServletUtils.renderString(httpServletResponse, JSONUtil.toJSONString(CommonResult.error(code,msg)));
}
}
同时也要加入自动配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and() //认证失败处理类
//基于 token,所以不需要 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests(....省略之前的配置信息
3、登出操作
关于用户退出操作,也需要自定义 LogoutSuccessHandler 进行操作
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Autowired
private TokenService tokenService;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
JwtUser jwtUser= tokenService.getLoginUser(httpServletRequest);
if (StringUtils.isNotNull(jwtUser)) {
//String username =
//删除 Redis 存储的用户信息
tokenService.delLoginUser(jwtUser.getToken());
}
//响应退出成功
ServletUtils.renderString(httpServletResponse, JSONUtil.toJSONString(CommonResult.success(null,"退出成功" )));
}
}
当然交给了 Spring 容器的 LogoutSuccessHandlerImpl 也要配置到 SpringSecurity:
@Overrideprotected void configure(HttpSecurity http) throws Exception {
....省略其他配置信息
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
}
到这里整个 SpringSecurity + JWT + redis 关于 API 接口安全认证的功能已经完毕了
明天计划的事情:
1、RabbitMQ 消息队列学习(关于短信验证码的发送,看之前有个师兄的日志是用消息队列异步发送的,先学习一下 消息队列)
2、短信接口 API 接入
3、Redis 对接口进行请求次数限制
遇到的问题:
无
收获:
学习了 SpringSecurity JWT 以及 Redis 实现 token 无状态登录认证
评论