发表于: 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 配置中:

@Override

protected void configure(HttpSecurity http) throws Exception {

   ....//省略之前的配置   

   http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}

AuthenticationEntryPoint :负责处理验证不通过( 这里自定义的验证机制,如果SpringSecurity 中没有注入 aut),给前端返回信息,从而做进一步处理

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPointSerializable {
   @Override
   public void commence(HttpServletRequest httpServletRequestHttpServletResponse httpServletResponseAuthenticationException e) throws IOExceptionServletException {
       int code = HttpStatus.UNAUTHORIZED.value();
       String msg = "认证失败,请重新登录";
       ServletUtils.renderString(httpServletResponseJSONUtil.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:

@Override

protected 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 无状态登录认证



返回列表 返回列表
评论

    分享到