Spring Security6 - 入门学习安全框架

Spring Security

Spring Security 依赖导入之后,再次访问我们编写的接口就要求我们登录获取 JSESSIONID,才能让我们访问项目下的接口(和资源),否则,就导航到登录页面。

后期通过 JWT(Token)来做身份验证,所以,会取消 JSESSIONID 管理,改用获取请求头 Token。

有以下两种异常情况:

  • 请求没有携带 Token,视为未登录,没有认证。
  • 请求携带了 Token,可以访问接口,已经登录,也就是已经认证,但可能权限不够。

没有认证

前端需要引导用户到登录页面进行认证,让服务器发放 Token,存储在浏览器中。

tip:[start]

Token 可以永久性存储,但一般都要设置过期时间。所以,需要借助 Redis 做缓存,把 Token 存起来并设置时间,在每次请求时都要第一时间检查 Token 是否过期。

tip:[end]

已经认证

获取 Token 存储的用户名、ID 等关键信息(如同身份证)。查询数据库中是否存在该用户,以及用户的权限,把权限交给 Spring Security,决定是否可以访问接口。

过滤链(FilterChain)

既然要做安全认证,那就应该在访问接口之前就拦截下来,进入认证流程。所以,在访问接口之前,要先经过一堆过滤器,拦截所有的没有配置放行的接口(URL)。

过滤链

以 JWT 认证机制为基础,在客户端发送一个请求时,携带上它浏览器存储的 Token 到请求头 Authorization 中,在经过我们自定义的 AuthJwtFilter,但这个过滤器要在 Spring Security 默认的过滤器之前工作,否则我们的过滤器就发挥不到作用。

tip:[start]

Token 存储在请求头哪里都无所谓,请求头你可以自定义一个,也可以填充规定里面的 Authorization 中,取决于你自己。

tip:[end]

file:[SecurityConfig]
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Resource
    private AuthJwtFilter authJwtFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // ......
        lit:[http.addFilterBefore(authJwtFilter, UsernamePasswordAuthenticationFilter.class);]
        return http.build();
    }

}

默认的认证过滤器就是 UsernamePasswordAuthenticationFilter,在它之前做 JWT 认证就可以了。

自定义过滤器

过滤器中拦截到请求之后,每次都进行身份认证,具体该如何做?

  1. 获取请求是否携带 Token,检验 Token 是否合法,等一系列前置工作。
  2. 解析 JWT 中存储的用户名,或者其他数据库中的主键值,必须要唯一的。
  3. 查询数据库中是否存在该用户。
  4. 如果存在该用户,给 SecurityContext 设置 UsernamePasswordAuthenticationToken,代表认证通过。
  5. 如果不存在该用户,不做第四步的工作,直接交给下一个过滤链。
file:[AuthJwtFilter]
@Component
public class AuthJwtFilter extends OncePerRequestFilter {

    @Resource
    private LoginMapper mapper;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 获取 Token
        String authorization = request.getHeader("Authorization");
        // 2. 解析 Token
        Claims claims = JwtUtil.parseJwt(authorization);

        // 3. Token 是否正确
        if (claims != null) {
            // 4. 校验 Token 是否过期
            String jwtId = claims.getId();
            Long expire = redisTemplate.getExpire(jwtId);
            // 5. Token 没有过期,代表可以认证
            if (expire != null && expire != -2) {
                // 从数据库中查询该用户对应的权限
                List<String> authorities = mapper.getAuthorities(Map.of("username", claims.get("username")));
                UserDetails details = JwtUtil.toUserDetails(claims, authorities);
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                // 6. 要让 Spring Security 知道认证通过,必须提供一个它定义的 UserDetails 类,构造器第一个和第三个参数必须给,一个是认证主体,一个是认证主体的权限
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(details, details.getPassword(), details.getAuthorities());
                context.setAuthentication(authentication);
                SecurityContextHolder.setContext(context);
            }
        }
        // 最重要的一步,放行
        filterChain.doFilter(request, response);
    }

}

AuthenticationEntryPoint

认证过程中,数据库连接错误,等内部问题,或者认证失败的问题都会进入这个处理器。

file:[AuthJwtEntryPointHandler]
@Component
public class AuthJwtEntryPointHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        AuthVoR vo = new AuthVoR();

        vo.setMessage("认证被拒绝,具体问题联系管理员。");
        vo.setCode(Codes.AUTHORITY_UNKNOWN_ERROR);

        response.getWriter()
                .write(JSON.toJSONString(vo));
    }

}

AccessDeniedHandler

Token 有效,但是权限不足进入这个处理器。

file:[AuthJwtAccessDeniedHandler]
@Component
public class AuthJwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        AuthVoR vo = new AuthVoR();
        vo.setCode(Codes.AUTH_NOT_ENOUGH);
        vo.setMessage("权限不足!请联系管理员升级权限。");
        response.getWriter()
                .write(JSON.toJSONString(vo));
    }

}

配置 Spring Security

file:[SecurityConfig]
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Resource
    private AuthJwtFilter authJwtFilter;

    @Resource
    add:[private AuthJwtEntryPointHandler authJwtEntryPointHandler;]:add

    @Resource
    add:[private AuthJwtAccessDeniedHandler authJwtAccessDeniedHandler;]:add

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        add:[http.exceptionHandling(conf -> conf
                .authenticationEntryPoint(authJwtEntryPointHandler)
                .accessDeniedHandler(authJwtAccessDeniedHandler)
        );]:add

        http.addFilterBefore(authJwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

}

自定义登录接口

Spring Security 提供了一个默认的登录接口,在实际的项目当中,我们不需要它提供的默认登录页面和接口以及逻辑。

UserDetailsService

实现 UserDetailsService,重写 loadUserByUsername 函数。我使用了 MybatisPlus,所以继承了 ServiceImpl。

file:[LoginServiceImpl]
@Service
public class LoginServiceImpl extends ServiceImpl<UserMapper, User> implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 从数据库中查询用户
        User user = findUserByFiled(username);
        // 2. 用户是否存在,不存在抛出异常,结束登录
        if (user == null) {
            throw new UsernameNotFoundException("用户名或密码错误!");
        }

        // 3. 返回规定的 User 实体类
        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                // 注意,密码的加密方式必须要和配置的加密方式一致,并且这不是一个明文密码,是加密密码
                .password(user.getPassword())
                .authorities(authorities.toArray(new String[0]))
                .build();
    }

    private User findUserByFiled(String field) {
        return query()
                .eq("username", field)
                .or()
                .eq("email", field)
                .or()
                .eq("phone", field)
                .one();
    }

}

AuthenticationSuccessHandler

登录成功进入这个处理器处理。我们在这里创建 JWT,并返回给前端存储。

file:[LoginSuccessHandler]
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        UserDetails details = (User) authentication.getPrincipal();

        String jwtUuid = UUID.randomUUID().toString();
        Long expire = JwtUtil.getExpire();
        String token = JwtUtil.createJwt(details, jwtUuid, expire);

        redisTemplate.opsForValue().set(jwtUuid, token, expire, TimeUnit.SECONDS);

        AuthVoR vo = new AuthVoR();
        vo.setToken(token);
        vo.setExpire(JwtUtil.getExpire());
        vo.setMessage("登录成功!");
        vo.setCode(Codes.SUCCESS);

        response.getWriter()
                .write(JSON.toJSONString(vo));
    }

}

AuthenticationFailureHandler

登陆失败的处理器。

file:[LoginFailureHandler]
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        AuthVoR vo = new AuthVoR();
        vo.setMessage("用户名或密码错误!");
        vo.setCode(Codes.PASSWORD_OR_USERNAME_ERROR);
        response.getWriter()
                .write(JSON.toJSONString(vo));
    }

}

配置 Spring Security

file:[SecurityConfig]
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Resource
    private AuthJwtFilter authJwtFilter;

    add:[@Resource
    private LoginSuccessHandler loginSuccessHandler;

    @Resource
    private LoginFailureHandler loginFailureHandler;]:add

    @Resource
    private AuthJwtEntryPointHandler authJwtEntryPointHandler;

    @Resource
    private AuthJwtAccessDeniedHandler authJwtAccessDeniedHandler;

    add:[@Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }]:add

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        add:[http.authorizeHttpRequests(auth -> {
            auth.requestMatchers("/api/auth/**").permitAll();
            auth.anyRequest().authenticated();
        });

        http.formLogin(conf -> conf
                .loginProcessingUrl("/api/auth/login")
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
        );]:add

        http.exceptionHandling(conf -> conf
                .authenticationEntryPoint(authJwtEntryPointHandler)
                .accessDeniedHandler(authJwtAccessDeniedHandler)
        );

        add:[http.csrf(AbstractHttpConfigurer::disable);
        http.cors(AbstractHttpConfigurer::disable);
        http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));]:add

        http.addFilterBefore(authJwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

}
posted @ 2023-08-25 18:40  Himmelbleu  阅读(107)  评论(0编辑  收藏  举报