前后端分离中 Spring Security 3.0 的基本使用

环境说明:

  1. JDK:17
  2. Spring Boot: 3.4.0
  3. Spring Security: 6.4

0X00 概要

目标:

  1. 实现一个登录接口且配置该接口不需要认证;
  2. 实现 JWT 的认证;
  3. 自定义认证失败;

0x01 基础配置

密码编码格式的设置

java 复制代码
// cn.keeploving.demo.config.SecurityConfig

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

实现 UserDetailsService,该接口仅有一个方法:loadUserByUsername,是根据登录的凭证(如:用户名、手机号、邮箱等)获取用信息的。

java 复制代码
// cn.keeploving.demo.config.SecurityConfig

@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
    // 这里使用 memory的,也可以自己继承进行实现
    // 这里就意味着有一个账号test,密码是 abc@123
    return new InMemoryUserDetailsManager(
            new User("test",
                    passwordEncoder.encode("abc@123"),
                    new ArrayList<>()
            )
    );
}

0x02 登录接口的实现

基本的登录接口的需要实现一下功能:

  1. 验证登录输入的验证码(如果有);
  2. 检查账号是否存在、密码是否正确,不存在(正确)则返回提示信息,如:用户名或密码错误;
  3. 检查账号的其他状态,返回对应的提示信息,如:账号已经锁定;
  4. 验证成功返回 JWT。

使用 Spring security 认证时需要先注入 AuthenticationManager,代码如下:

java 复制代码
// cn.keeploving.demo.config.SecurityConfig

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
    return configuration.getAuthenticationManager();
}

然后使用 AuthenticationManager 的 authenticate 方法进行认证是会抛出异常,如下:

  • BadCredentialsException:当登录凭证(如密码)错误时抛出的异常
  • UsernameNotFoundException:未找到账号时抛出异常
  • DisabledException:账号被禁用时抛出的异常
  • LockedException:账号被锁定时抛出的异常

还有更多可能排除的异常,但是这几个异常是和账号信息有关的,它们都是 AccountStatusException 的子类,所以登录时可以捕获它们来返回登录失败信息,具体的代码如下:

java 复制代码
// cn.keeploving.demo.controller.AuthController

@RestController
@RequestMapping("auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public String login(@RequestBody LoginDto loginDto) {
        // TODO:验证验证码

        // 登录认证
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
        try {
            Authentication authenticate = authenticationManager.authenticate(token);
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            // 生成JWT并返回
            Date now = new Date();
            return Jwts.builder().signWith(ValuePool.KEY)
                    // token 携带的信息
                    .claim("username", loginDto.getUsername())
                    // 设置过期时间
                    .setExpiration(new Date(now.getTime() + 8 * 60 * 60 * 1000))
                    .compact();
        } catch (BadCredentialsException | UsernameNotFoundException exp) {
            return "用户名或密码错误";
        } catch (LockedException | AccountExpiredException exp) {
            return "账号信息异常,请联系管理员处理";
        }
    }
}

此时请求 /auth/login 接口会返回 401。Spring security 默认会拒绝非 /login 的请求并跳转到 /login 页面,可以在该页面使用配置的 test 账号进行登录;

前后端分离需要关闭 form 登录并且开放 /auth/login 接口,配置如下:

java 复制代码
// cn.keeploving.demo.config.SecurityConfig

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http.csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(registry -> {
                // 允许 /auth/login 接口访问
                registry.requestMatchers("/auth/login").permitAll();
                // 其他的需要认证
                registry.anyRequest().authenticated();
            })
            // 关闭 form
            .formLogin(AbstractHttpConfigurer::disable)
            .build();
}

配置 /auth/login 不需要认证就可以访问,其他的请求需要认证才可以访问

0x03 认证 JWT

通过 Filter 对请求进行拦截获取 JWT,获取到 JWT 后要对 JWT 进行解析、判断,符合要求后就要生成 Spring Security 需要的 Token 信息。具体实现的代码如下:

java 复制代码
// cn.keeploving.demo.filter.JwtFilter

public class JwtFilter extends HttpFilter {

    private final JwtParser build = Jwts.parserBuilder().setSigningKey(ValuePool.KEY).build();

    private static final Logger LOG = LoggerFactory.getLogger(JwtFilter.class);
    private final UserDetailsService userDetailsService;

    public JwtFilter(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = doGetJwtToken(request);
        if (token == null) {
            // 交给后续处理
            chain.doFilter(request, response);
            return;
        }
        try {
            // 解析 TOKEN
            Jws<Claims> claimsJws = build.parseClaimsJws(token);
            String username = claimsJws.getBody().get("username", String.class);
            // 检验 TOKEN
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (userDetails == null) {
                throw new BadCredentialsException("异常的认证信息");
            }

            // 生成上下文使用的 TOKEN
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, "", userDetails.getAuthorities());
            authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authToken);
        } catch (Exception exp) {
            LOG.error("JWT解析失败: ", exp);
        }
        chain.doFilter(request, response);
    }

    private String doGetJwtToken(HttpServletRequest request) {
        String authorization = request.getHeader("Authorization");
        if (authorization == null) {
            return null;
        }
        return authorization.replace("Bearer ", "");
    }

}

开发完 Filter 后还需要配置 Filter 的顺序,具体配置如下:

java 复制代码
// cn.keeploving.demo.config.SecurityConfig

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService service) throws Exception {
    return http.csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(registry -> {
                registry.requestMatchers("/auth/login").permitAll();
                registry.anyRequest().authenticated();
            })
            .formLogin(AbstractHttpConfigurer::disable)
            // 添加 Filter
            .addFilterBefore(new JwtFilter(service), UsernamePasswordAuthenticationFilter.class)
            .build();
}

开发一个 self 接口获取用户登录成功之后的信息:

java 复制代码
// cn.keeploving.demo.controller.AuthController

@GetMapping("/self")
public Object self() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    Object principal = authentication.getPrincipal();
    return principal;
}

携带登录成功后乡响应的 jwt 即可拿到登录用户的 username。

0x04 认证失败的处理

默认认证失败后响应空白内容并且将响应状态设置为 403,部分项目会有自己的定制化需求,可以配置 exceptionHandling 进行设置。

实现 AccessDeniedHandler 和 AuthenticationEntryPoint 就可以实现请求被拒绝、认证异常的响应,具体代码如下:

java 复制代码
public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        doErrorResp(response);
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        doErrorResp(response);
    }

    private static void doErrorResp(HttpServletResponse response) throws IOException {
        response.setStatus(HttpStatus.FORBIDDEN.value());

        response.setContentType("application/json");
        response.setHeader("content-type", "application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write("拒绝访问");
        response.getWriter().flush();
    }

配置如下:

java 复制代码
// cn.keeploving.demo.config.SecurityConfig

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService service) throws Exception {
    return http.csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(registry -> {
                registry.requestMatchers("/auth/login").permitAll();
                registry.anyRequest().authenticated();
            })
            .exceptionHandling(config -> {
                CustomAccessDeniedHandler handler = new CustomAccessDeniedHandler();
                config.accessDeniedHandler(handler);
                config.authenticationEntryPoint(handler);
            })
            .formLogin(AbstractHttpConfigurer::disable)
            .addFilterBefore(new JwtFilter(service), UsernamePasswordAuthenticationFilter.class)
            .build();
}

请求 /auth/self 不携带 JWT 或者携带错误的 JWT 都会响应:拒绝访问

相关推荐
计算机-秋大田22 分钟前
基于微信小程序的电子竞技信息交流平台设计与实现(LW+源码+讲解)
spring boot·后端·微信小程序·小程序·课程设计
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS景区民宿预约系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
精通HelloWorld!8 小时前
使用HttpClient和HttpRequest发送HTTP请求
java·spring boot·网络协议·spring·http
拾忆,想起9 小时前
如何选择Spring AOP的动态代理?JDK与CGLIB的适用场景
spring boot·后端·spring·spring cloud·微服务
customer0811 小时前
【开源免费】基于SpringBoot+Vue.JS美食推荐商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
一 乐12 小时前
基于微信小程序的酒店管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·微信小程序·酒店管理系统
小万编程17 小时前
【2025最新计算机毕业设计】基于SpringBoot+Vue家政呵护到家护理服务平台(高质量源码,可定制,提供文档,免费部署到本地)
java·vue.js·spring boot·计算机毕业设计·java毕业设计·web毕业设计
XYu1230121 小时前
Spring Boot 热部署实现指南
java·ide·spring boot·intellij-idea
是小崔啊1 天前
Spring Boot - 数据库集成07 - 数据库连接池
数据库·spring boot·oracle
细心的莽夫1 天前
SpringBoot 基础(Spring)
spring boot·后端·spring