Spring Security 学习日记四

94 阅读4分钟

自定义认证(前后分离)

工具类

//主要处理redis的事务
@Component
public class RedisUtils {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ObjectMapper objectMapper;
​
    public <T>String setRedis(String key , T base) {
        String json = null;
        try {
            json = objectMapper.writeValueAsString(base);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        redisTemplate.opsForValue().set(GlobalConstants.REDIS_USER_KEY_PREFIX+key,json);
        return json;
    }
​
    public LoginUser getRedis(String key) throws IOException {
            String json = redisTemplate.opsForValue().get(GlobalConstants.REDIS_USER_KEY_PREFIX + key);
            LoginUser loginUser = null;
            if(json != null){
                loginUser = new LoginUser();
                CustomAuthority customAuthority = objectMapper.readValue(json,CustomAuthority.class);
                List<CustomAuthority> customAuthorities = objectMapper.readValue(customAuthority.getAuthority(), new TypeReference<List<CustomAuthority>>(){});
                loginUser.setAuthorities(customAuthorities);
            }
            return loginUser;
    }
​
    public boolean delete(String key){
        Boolean delete = redisTemplate.delete(GlobalConstants.REDIS_USER_KEY_PREFIX + key);
        return delete;
    }
}
@Component
public class WebUtils {
    @Autowired
    private ObjectMapper objectMapper;
    public void writer(HttpServletResponse response, HttpResult result) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(result));
    }
}
public class JwtUtils {
    public static <T>String  getSecretKey(String base){
        Map<String, Object> map = new HashMap<String, Object>(){
            {
                put("base",base);
                put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24);
            }
        };
        String token = JWTUtil.createToken(map, GlobalConstants.JWT_SIGNATURE_KEY.getBytes());
        return token;
    }
​
    public static Object  parseToken(String token){
        JWT jwt = JWTUtil.parseToken(token);
        Object uid = jwt.getPayload().getClaim("base");
        return uid;
    }
}
//使用个这个工具类之前,我们还要引入一个依赖
<dependency>
   <groupId>cn.hutool</groupId>
   <artifactId>hutool-all</artifactId>
   <version>5.8.16</version>
</dependency>

配置redis

redis:
  host: localhost
  port: 6379
  password: 123456
  lettuce:
    pool:
      max-active: 8
      max-idle: 8
      min-idle: 0
      max-wait: 200ms
  database: 0

配置类

@Bean
AuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception {
    return (AuthenticationManager) httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
            .build();
}
  @Bean
    public SecurityFilterChain chain(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/toLogin").permitAll()
                .anyRequest().authenticated()
                .csrf().disable();
        return http.build();
    }

重写方法

前端的信息会以json的方式传给后端,后端的数据也是以json的方式传递给前端。所以我们用Spring Security默认的UsernamePasswordAuthenticationFilter是不行的,所以我们第一步就是自己写一个Filter,我们来一步步分析。

@Component
public class MyUsernamePasswordLoginFilter extends AbstractAuthenticationProcessingFilter {
    //首先我们就是继承AbstractAuthenticationProcessingFilter
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private MyLoginSuccessHandler myLoginSuccessHandler;
    @Autowired
    private MyLoginFailureHandler myLoginFailureHandler;
​
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/toLogin", "POST");//它只会拦截"/toLogin"登录的路径,和POST请求方式
    
    private boolean postOnly = true;
​
    public MyUsernamePasswordLoginFilter(AuthenticationManager antPathRequestMatcher) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER,antPathRequestMatcher);
    }
​
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, RuntimeException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());//判断postOnly是否为真,和前端的请求是否为“POST”
        } else {
            User user = objectMapper.readValue(request.getInputStream(), User.class);
            //request.getInputStream()获取从前端传过来的json数据
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(user.getName(), user.getPassword());//将数据进行封装
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
    
    //成功后调用
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        myLoginSuccessHandler.onAuthenticationSuccess(request,response,authResult);//自定义登录成功类
    }
​
    //失败后调用
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        myLoginFailureHandler.onAuthenticationFailure(request,response,failed);//自定义登录失败类
    }
}

自定义的成功和失败类

如前面少些不同,因为前后的分离了,所以我们后端就不再做跳转的功能。我们只向前端发送一个json数据,json中包含token(令牌)。以后前端发送请求必须携带token,否则就视为没有认证。

@Component
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private RedisUtils redisUtils;//redis工具类
    @Autowired
    private WebUtils webUtils;
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        String token = JwtUtils.getSecretKey(loginUser.getUser().getId());
        redisUtils.setRedis(loginUser.getUser().getId(),objectMapper.writeValueAsString(loginUser.getAuthorities()));
        webUtils.writer(response,HttpResult.ok("登录成功", HttpToken.token(token)));
    }
}
@Component
public class MyLoginFailureHandler implements AuthenticationFailureHandler {
​
    @Autowired
    private WebUtils webUtils;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        HttpResult httpResult = null;
        if(exception instanceof LockedException){
            httpResult = HttpResult.fail("账户被锁定!",9999);
        }
        else if(exception instanceof BadCredentialsException){
            httpResult = HttpResult.fail(exception.getLocalizedMessage(),9997);//用户名或密码错误
        }
        webUtils.writer(response,httpResult);
    }
}

自定义注销

配置类

@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;
​
@Bean
public SecurityFilterChain chain(HttpSecurity http) throws Exception {
    http
            .cors().and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/toLogin").permitAll()
            .antMatchers("/toLogout").permitAll()
            .anyRequest().authenticated()
            .and()
            .logout()
            .logoutUrl("/toLogout")
            .logoutSuccessHandler(myLogoutSuccessHandler)
            .csrf().disable();
    return http.build();
}

MyLogoutSuccessHandler

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    private WebUtils webUtils;
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String token = request.getHeader(GlobalConstants.HEADER_TOKEN_KEY);//从获取前端传过来的token
        if(token!=null){
            Object key = JwtUtils.parseToken(token);
            redisUtils.delete((String) key);
        }//我获取token,从token中获取信息,根据信息删除redis中的数据
        webUtils.writer(response, HttpResult.ok("注销成功"));
    }
}

异常处理

配置类

    @Autowired
    private MyDeniedHandler myDeniedHandler;
    @Autowired
    private MyEntryPoint myEntryPoint;
    
    @Bean
    public SecurityFilterChain chain(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/captcha").permitAll()
                .antMatchers("/toLogin").permitAll()
                .antMatchers("/toLogout").permitAll()
                .antMatchers("/hello").anonymous()
                .anyRequest().authenticated()
                .and()
                .logout()
                .logoutUrl("/toLogout")
                .logoutSuccessHandler(myLogoutSuccessHandler)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(myDeniedHandler)//处理权限异常
                .authenticationEntryPoint(myEntryPoint)//处理认证异常
                .and()
                .csrf().disable();
        return http.build();
    }
}

MyDeniedHandler

@Component
public class MyDeniedHandler implements AccessDeniedHandler{
    @Autowired
    private WebUtils webUtils;
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        HttpResult httpResult = null;
        httpResult = HttpResult.fail("权限不足",8998);
        webUtils.writer(response,httpResult);
    }
}

MyEntryPoint

@Component
public class MyEntryPoint implements AuthenticationEntryPoint {
    @Autowired
    private WebUtils webUtils;
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        HttpResult httpResult = null;
        httpResult = HttpResult.fail(authException.getLocalizedMessage(),7999);
        webUtils.writer(response,httpResult);
    }
}

自定义授权

配置类

@Autowired
private MyBasicAuthenticationFilter myBasicAuthenticationFilter;
​
 @Bean
    public SecurityFilterChain chain(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/toLogin").permitAll()
                .antMatchers("/toLogout").permitAll()
                .anyRequest().authenticated()
                .and()
                .logout()
                .logoutUrl("/toLogout")
                .logoutSuccessHandler(myLogoutSuccessHandler)
                .and()
                .addFilterBefore(myBasicAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .accessDeniedHandler(myDeniedHandler)
                .authenticationEntryPoint(myEntryPoint)
                .and()
                .csrf().disable();
        return http.build();
    }

MyBasicAuthenticationFilter

@Component
public class MyBasicAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private RedisUtils redisUtils;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取请求头中的token
        String token = request.getHeader(GlobalConstants.HEADER_TOKEN_KEY);
        if(!StringUtils.hasText(token)){
            filterChain.doFilter(request,response);
            return;
        }
        Object uid = JwtUtils.parseToken(token);
        LoginUser loginUser = redisUtils.getRedis(uid.toString());
        if(Objects.nonNull(loginUser)){
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(uid, (Object) token, loginUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request,response);
    }
}