微服务安全——SpringSecurity6详解

文章目录

说明

本文使用的是Security6的版本,先介绍SpringSecurity的使用,然后再去介绍OAuth2。

SpringSecurity也只是入门知识

  • Authentication(认证,解决who are you? )
  • Authorization (访问控制,也就是what are you allowed to do?)

版本:SpringBoot3.1.4、Security6.1.4

SpringSecurity认证

快速开始

创建一个简单的SpringBoot应用

引入依赖

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.4</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>


<dependencies>
    <!-- 接入spring security-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

编写一个简单的Controller

java 复制代码
@RestController
@RequestMapping("/admin")
public class AdminController {
    @GetMapping("/demo")
    public String demo() {
    return "spring security demo";
    }
}

启动项目后测试接口调用

引入Spring Security依赖之后 ,访问 API 接口时,需要首先进行登录,才能进行访问。

测试 http://localhost:8080/admin/demo ,会跳转到登录界面

页面生成源码:DefaultLoginPageGeneratingFilter#generateLoginPageHtml

用户名密码认证Filter: UsernamePasswordAuthenticationFilter

需要登录,默认用户名:user,密码可以查看控制台日志获取

登录之后跳转回请求接口

日志中,打印所有要执行的Filter如下

bash 复制代码
2024-07-22T08:27:34.023+08:00 INFO 10172 --- [           main]o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@1f939a0f, 
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@45658133, 
org.springframework.security.web.context.SecurityContextHolderFilter@1e1eeedd, 
org.springframework.security.web.header.HeaderWriterFilter@1fbf088b, 
org.springframework.security.web.csrf.CsrfFilter@1c3259fa, 
org.springframework.security.web.authentication.logout.LogoutFilter@4c6b4ed7, 
# 核心Filter,校验用户名密码的
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@71ed560f,  
# 默认登录页面
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@3aaa3c39, 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@430b2699, 
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@40247d48, 
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7ec95456, 
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@428bdd72, 
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@67536ae0, 
# 处理异常的Filter
org.springframework.security.web.access.ExceptionTranslationFilter@2954f6ab, 
# Security6,把鉴权相关的专门抽出来 成了一个Filter
org.springframework.security.web.access.intercept.AuthorizationFilter@64c781a9]

退出登录

Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送 http://localhost:8080/logout 退出请求即可。

设置用户名密码

了解即可

基于application.yml方式

可以在application.yml中自定义用户名密码

yml 复制代码
spring:
  # Spring Security 配置项,对应 SecurityProperties 配置类
  security:
    user:
      name: user   # 用户名
      password: 123456  # 密码
      roles:   # 拥有角色
        - admin

原理:

默认情况下,UserDetailsServiceAutoConfiguration自动化配置类,会创建一个内存级别的InMemoryUserDetailsManager对象,提供认证的用户信息。

  • 添加 spring.security.user 配置项,UserDetailsServiceAutoConfiguration 会基于配置的信息在内存中创建一个用户User。
  • 未添加 spring.security.user 配置项,UserDetailsServiceAutoConfiguration 会自动在内存中创建一个用户名为 user,密码为 UUID 随机的用户 User

基于Java Bean配置方式

java 复制代码
@Configuration
@EnableWebSecurity  //开启spring sercurity支持
public class SecurityConfig {


    /**
     * 配置用户信息
     * 我们正常的用法是自定义一个类,实现UserDetailsService接口,再通过username去查询DB,再封装一个UserDetails对象返回。
     * 这里就简单实现,直接指定
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        //使用默认加密方式bcrypt对密码进行加密,添加用户信息
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("hushang")
                .password("123456")
                .roles("user")
                .build();

        UserDetails admin = User.withUsername("admin")
                .password("{noop}123456") //对密码不加密
                .roles("admin", "user")
                .build();
        // 返回一个UserDetailsService的实现类InMemoryUserDetailsManager,从类名可以看出来是基于内存的
        return new InMemoryUserDetailsManager(user, admin);
    }
}

另一种方式

java 复制代码
@Configuration
@EnableWebSecurity  //开启spring sercurity支持
public class SecurityConfig {


    /**
     * 配置用户信息
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        //使用默认加密方式bcrypt对密码进行加密,添加用户信息
        //加密方式1:{id}encodedPassword ,id为加密算法类型
//        UserDetails user = User.withDefaultPasswordEncoder()
//                .username("hushang")
//                .password("123456")
//                .roles("user")
//                .build();
//
//        UserDetails admin = User.withUsername("admin")
//                .password("{noop}123456") //noop表示对密码不加密
//                .roles("admin", "user")
//                .build();

        // 加密方式2: passwordEncoder().encode("123456")
        UserDetails user = User
                .withUsername("hushang")
                .password(passwordEncoder().encode("123456"))
                .roles("user")
                .build();
        UserDetails admin = User.withUsername("admin")
                //指定加密算法对密码加密
                .password(passwordEncoder().encode("123456"))
                .roles("admin", "user")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        //return NoOpPasswordEncoder.getInstance();  //不加密
        return new BCryptPasswordEncoder();  //加密方式bcrypt
    }
}

设置加密方式

方式1:{id}encodedPassword

Spring Security密码加密格式为:{id}encodedPassword

java 复制代码
UserDetails user = User.withUsername("user")
      .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
      .roles("USER")
      .build();
UserDetails admin = User.withUsername("admin")
        .password("{noop}123456") //noop表示对密码不加密
        .roles("admin", "user")
        .build();

如果密码不指定{id}会抛异常:

方式2: passwordEncoder().encode("123456")

java 复制代码
@Configuration
@EnableWebSecurity  //开启spring sercurity支持
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {

        UserDetails user = User
                .withUsername("hushang")
                .password(passwordEncoder().encode("123456"))
                .roles("user")
                .build();

        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        //return NoOpPasswordEncoder.getInstance();  //不加密
        return new BCryptPasswordEncoder();  //加密方式bcrypt
    }
}

Spring Security支持的加密方式可以通过PasswordEncoderFactories查看

java 复制代码
public final class PasswordEncoderFactories {
    private PasswordEncoderFactories() {
    }

    public static PasswordEncoder createDelegatingPasswordEncoder() {
        // 默认使用的是BCryptPasswordEncoder
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new LdapShaPasswordEncoder());
        encoders.put("MD4", new Md4PasswordEncoder());
        encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
        encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
        encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
        encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
        encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new StandardPasswordEncoder());
        encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
        encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
        return new DelegatingPasswordEncoder(encodingId, encoders);
    }
}

测试类

java 复制代码
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public static void main(String[] args) {
    //加密
    String passwd = BCrypt.hashpw("123456",BCrypt.gensalt());
    System.out.println(passwd);

    //校验
    boolean checkpw = BCrypt.checkpw("123456", "$2a$10$KfdyA40l4iElg7ox9GLR9.4ujIv6q9EfOpcRwrM7zYQrDHZuYoIui");
    System.out.println(checkpw);


    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    // 加密
    String encode = passwordEncoder.encode("123455");
    System.out.println(encode);

    // 校验
    boolean matches = passwordEncoder.matches("123455", "$2a$10$7ZeilxBWjUfv8XP7tlxZK.GKQPHG4dETOOYPscDZy1lVpK0PLGy96");
    System.out.println(matches);

}

自定义用户加载方式

需要自定义从数据库获取用户信息,可以实现UserDetailsService接口

java 复制代码
@Configuration
@EnableWebSecurity  //开启spring sercurity支持
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder(){
        //return NoOpPasswordEncoder.getInstance();  //不加密
        return new BCryptPasswordEncoder();  //加密方式bcrypt
    }
}

认证流程中 UsernamePasswordAuthenticationFilter 会找UserDetailsService接口类型,会调用到下面我们重写的方法中,把界面上输入的用户名传递过来

java 复制代码
package com.tuling.helloworld.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class TulingUserDetailService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        
        //TODO  根据用户名可以从数据库获取用户信息,角色以及权限信息
        
        // 模拟从数据库获取了用户信息,并封装成UserDetails对象
        // 这里的API方法和security5没什么改变,
        UserDetails user = User
                .withUsername("hushang")
                .password(passwordEncoder.encode("123456"))
                .roles("user")
                .build();

        return user;
    }
}



/*
直接把密文存入也可以
UserDetails user = User
                .withUsername("admin")
                .password("$2a$10$KfdyA40l4iElg7ox9GLR9.4ujIv6q9EfOpcRwrM7zYQrDHZuYoIui")
                .roles("user")
                .build();
*/

补充知识点

java 复制代码
// 伪代码,roles()指定角色的方法,其实是把我们的角色封装了一下,"ROLE_" + role  封装成了一个Authority权限对象
roles(String... roles) {
    List<GrantedAuthority> authorities = new ArrayList(roles.length);
    authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}

复制上一篇笔记中的内容,用户状态的判断

在生产环境下还有可能出现用户被禁用等等这些场景,我们这里也需要考虑进去,就比如某个用户现在数据库中是禁用状态,那么这里就不能让认证通过

org.springframework.security.core.userdetails.User() 对象的构造方法,除了 用户名+密码+一个角色封装之后的权限集合之外

java 复制代码
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }

其实还有一个更为复杂的构造方法

java 复制代码
public User(String username,			// 用户名
            String password,			// 密码
            boolean enabled,			// 是否可用
			boolean accountNonExpired,	// 账号过期
            boolean credentialsNonExpired,	// 凭证过期
			boolean accountNonLocked,		// 账号锁定
            Collection<? extends GrantedAuthority> authorities) {
    ......
}

我们可以进行测试,现在将其中一个值该为false,然后测试登录认证

就会发现即使输入了正确的用户名和密码,还是登录不进去

自定义登录页面

本小节需了解SpringSecurity的过滤器链的配置

Spring Security默认登录页面通过DefaultLoginPageGeneratingFilter#generateLoginPageHtml生成

编写登录页面

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/user/login" method="post">
        用户名:<input type="text" name="username"/><br/>
        密码:<input type="password" name="password"/><br/>
        <input type="submit" value="提交"/>
    </form>
</body>
</html>

配置Spring Security的过滤器链

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    //表单提交
    // /user/login接口不是我们程序员定义的接口
    http.formLogin((formLogin) -> formLogin
            .loginPage("/login.html") //指定自定义登录页面地址
            .loginProcessingUrl("/user/login")//登录访问路径:前台界面提交表单之后跳转到这个路径进行UserDetailsService的验证,必须和表单提交接口一样
            .defaultSuccessUrl("/admin/demo")//认证成功之后跳转的路径
    );
    
    //对请求进行访问控制设置
    http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
            //设置哪些路径可以直接访问,不需要认证
            .requestMatchers("/login.html","/user/login").permitAll()
            .anyRequest().authenticated() //其他路径的请求都需要认证
    );
    
    //关闭跨站点请求伪造csrf防护
    http.csrf((csrf) -> csrf.disable());

    return http.build();
}

测试 http://localhost:8080/admin/demo ,会跳转到自定义登录界面

前后端分离认证

表单登录配置模块提供了successHandler()failureHandler()两个方法,分别处理登录成功和登录失败的逻辑。

其中,successHandler()方法带有一个Authentication参数,携带当前登录用户名及其角色等信息;而failureHandler()方法携带一个AuthenticationException异常参数。

java 复制代码
//前后端分离认证逻辑
http.formLogin((formLogin) -> formLogin
        .loginProcessingUrl("/login") //登录访问接口
        .successHandler(new LoginSuccessHandler()) //登录成功处理逻辑
        .failureHandler(new LoginFailureHandler()) //登录失败处理逻辑
);



/**
 * 认证成功处理逻辑,我们可以在这里生成token返回给前端
 * Authentication参数,携带当前登录用户名及其角色等信息
 */
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().write("登录成功");
    }
}

//
/**
 * 认证失败处理逻辑
 */
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // TODO
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().write("登录失败");
        exception.printStackTrace();
    }
}

认证流程

SpringSecurity授权

授权的方式包括 web授权和方法授权:

  • web授权是通过url拦截进行授权
  • 方法授权是通过方法拦截进行授权

web授权:基于url的访问控制

Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护 ,Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。配置顺序会影响之后授权的效果,越是具体的应该放在前面,越是笼统的应该放到后面。

java 复制代码
import com.tuling.helloworld.handler.BussinessAccessDeniedHandler;
import com.tuling.helloworld.handler.LoginFailureHandler;
import com.tuling.helloworld.handler.LoginSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        //前后端分离认证逻辑
        http.formLogin((formLogin) -> formLogin
                .loginProcessingUrl("/login")
                .successHandler(new LoginSuccessHandler())
                .failureHandler(new LoginFailureHandler())
        );

        //对请求进行访问控制设置
        http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                //设置哪些路径可以直接访问,不需要认证
                .requestMatchers("/login").permitAll()  //不需要认证
                .requestMatchers("/index").hasRole("user")  //需要user角色,底层会判断是否有ROLE_admin权限
                .requestMatchers("/index2").hasRole("admin")
                .requestMatchers("/user/**").hasAuthority("user:api") //需要user:api权限
                .requestMatchers("/order/**").hasAuthority("order:api")
                .anyRequest().authenticated()  //其他路径的请求都需要认证,仅仅认证通过后就可以了,不会去进行鉴权
        );
        return http.build();
    }


    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("hushang")
                .password("123456")
                .roles("user")
                .build();

        UserDetails admin = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("123456")
                // 注意: roles和authorities不能同时配置,同时配置后者会覆盖前者的权限
            	// roles()方法底层对我们角色加一个前缀ROLE_,然后还是调用的authorities()。而authorities()每次都是生成一个新数组赋值
                .authorities("ROLE_admin","ROLE_user","user:api","order:api")
                .build();

        return new InMemoryUserDetailsManager(user,admin);
    }
}

自定义授权失败异常处理

使用 Spring Security 时经常会看见 403(无权限)。Spring Security 支持自定义权限受限处理,需要实现 AccessDeniedHandler接口

  1. 我们先自定义一个类,实现AccessDeniedHandler接口

    java 复制代码
    public class BussinessAccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.setContentType("text/html;charset=utf-8");
            response.getWriter().write("没有访问权限");
            accessDeniedException.printStackTrace();
        }
    }
  2. 在配置类中指定我们上面创建的类

    java 复制代码
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
        //前后端分离认证逻辑
        http.formLogin((formLogin) -> formLogin
                       .loginProcessingUrl("/login") 
                       .successHandler(new LoginSuccessHandler()) 
                       .failureHandler(new LoginFailureHandler()) 
                      );
    
        //对请求进行访问控制设置
        http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                                   .requestMatchers("/login").permitAll()  
                                   .requestMatchers("/index").hasRole("user")  
                                   .requestMatchers("/index2").hasRole("admin")
                                   .requestMatchers("/user/**").hasAuthority("user:api") 
                                   .requestMatchers("/order/**").hasAuthority("order:api")
                                   .anyRequest().authenticated() 
                                  );
    
        //关闭跨站点请求伪造csrf防护
        http.csrf((csrf) -> csrf.disable());
        
        //访问受限后的异常处理
        http.exceptionHandling((exceptionHandling) ->
                               exceptionHandling.accessDeniedHandler(new BussinessAccessDeniedHandler())
                              );
        return http.build();
    
    }

更全面一点的写法如下

java 复制代码
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class BussinessAccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().write("没有访问权限");
        accessDeniedException.printStackTrace();
    }
}
java 复制代码
import com.alibaba.fastjson.JSON;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;


@Component
public class LoginAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 如果验证失败,统一返回JSON串,并将状态码设置为401,表示未授权
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        PrintWriter out = response.getWriter();
        Map<String,Object> data = new HashMap<>();
        data.put("path", request.getRequestURI());
        data.put("time", LocalDateTime.now().toString());
        data.put("errCode", HttpStatus.UNAUTHORIZED.value());
        data.put("errMsg", HttpStatus.UNAUTHORIZED.getReasonPhrase());
        out.write(JSON.toJSONString(data));
        out.flush();
        out.close();
    }
}
java 复制代码
//访问受限后的异常处理
        http.exceptionHandling((exceptionHandling) -> exceptionHandling
                .authenticationEntryPoint(loginAuthenticationEntryPoint)
                .accessDeniedHandler(bussinessAccessDeniedHandler)
        );

方法授权:基于注解的访问控制

了解即可,注解的方式用的少

Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解。这三种注解默认都是没有启用的,需要通过@EnableGlobalMethodSecurity来进行启用。

java 复制代码
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true, securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
}

接下来是Controller层 注解的使用

java 复制代码
//Controller
@RolesAllowed({"ROLE_user","ROLE_admin"})  //配置访问此方法时应该具有的角色
@GetMapping("/index5")
public String index5(){
    return "index5";
}

@Secured("ROLE_admin")    //配置访问此方法时应该具有的角色
@GetMapping("/index6")
public String index6(){
    return "index6";
}

Spring Security中定义了四个支持使用表达式的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。

java 复制代码
@PreAuthorize("hasRole('ROLE_admin') and #id<10 ") //访问此方法需要具有admin角色,同时限制只能查询id小于10的用户
@GetMapping("/findUserById")
public String findById(long id) {
    //TODO 查询数据库获取用户信息
    return "success";
}

利用过滤器实现动态权限控制

Spring Security从5.5之后动态权限控制方式已经改变。

5.5之前需要实现接口:

  • FilterInvocationSecurityMetadataSource: 获取访问URL所需要的角色信息
  • AccessDecisionManager: 用于权限校验,失败抛出AccessDeniedException 异常

5.5之后,利用过滤器动态控制权限,在AuthorizationFilter中,只需要实现接口AuthorizationManager,如果没有权限,抛出AccessDeniedException异常

权限校验核心逻辑:

org.springframework.security.web.access.intercept.AuthorizationFilter#doFilter

》org.springframework.security.authorization.AuthorityAuthorizationManager#check

》org.springframework.security.authorization.AuthoritiesAuthorizationManager#isAuthorized

Spring Security整合JWT

接下来的案例是Spring Security整合JWT实现自定义登录认证

自定义登录认证的业务需求

用spring boot + spring security+JWT 框架实现登录认证授权功能,用户登录成功后,服务端利用JWT生成token

之后客户端每次访问接口,都需要在请求头上添加Authorization:Bearer token 的方式传值到服务器端,服务器端再从token中解析和校验token的合法性

如果合法,则取出用户数据,保存用户信息,不需要在校验登录;否则就需要重新登录

JWT详解

什么是JWT

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。

JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

官网: https://jwt.io/

标准: https://tools.ietf.org/html/rfc7519

JWT令牌的优点:

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  4. 资源服务使用JWT可不依赖授权服务即可完成授权。

缺点:

  1. JWT令牌较长,占存储空间比较大。

  2. 安全性取决于密钥管理

    JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等。

  3. 无法撤销

    由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户账户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。

使用 JWT 主要用来做下面两点:

  • 认证(Authorization):这是使用 JWT 最常见的一种情况,一旦用户登录,后面每个请求都会包含 JWT,从而允许用户访问该令牌所允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小。
  • 信息交换(Information Exchange):JWT 是能够安全传输信息的一种方式。通过使用公钥/私钥对 JWT 进行签名认证。此外,由于签名是使用 head 和 payload 计算的,因此你还可以验证内容是否遭到篡改。

JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)。

头部(header)

头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如HMACSHA256或RSA)等。这也可以被表示成一个JSON对象:

json 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:

bash 复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

载荷(payload)

第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

  • 标准中注册的声明(建议但不强制使用)

    iss: jwt签发者

    sub: jwt所面向的用户

    aud: 接收jwt的一方

    exp: jwt的过期时间,这个过期时间必须要大于签发时间

    nbf: 定义在什么时间之前,该jwt都是不可用的.

    iat: jwt的签发时间

    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

  • 公共的声明

    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密

  • 私有的声明

    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

json 复制代码
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

然后将其进行base64加密,得到Jwt的第二部分:

复制代码
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

签名(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret(盐,一定要保密)

这个部分需要base64加密后的header、base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:

javascript 复制代码
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'hushang'); // km962Qj9Dvkjovs-ZNoALRsB4WRBKh-LjSuMe4yiIHs

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.km962Qj9Dvkjovs-ZNoALRsB4WRBKh-LjSuMe4yiIHs

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

如何应用

一般是在请求头里加入Authorization,并加上Bearer标注:

javascript 复制代码
fetch('api/user/1', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
})

服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:

自定义登录核心实现

maven依赖

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <groupId>com.tuling</groupId>
    <artifactId>jwtdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jwtdemo</name>
    <description>jwtdemo</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <!--JWT依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.1.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

创建一个JWT的工具类

java 复制代码
import com.alibaba.fastjson.JSON;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.MacSigner;

import java.util.Map;

public class JWTUtils {

    /**
     * 创建JWT
     * @param secret
     * @param claims 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
     * @return
     */
    public static String getAccessToken(String secret, Map<String, Object> claims){

        // 指定签名的时候使用的签名算法。
        MacSigner rsaSigner=new MacSigner(secret);
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(claims), rsaSigner);
        return jwt.getEncoded();
    }

    public static Map<String,Object> parseToken(String token){
        Jwt jwt = JwtHelper.decode(token);
        return JSON.parseObject(jwt.getClaims());
    }

    /**
     * 根据传入的token过期时间判断token是否已过期
     * @param expiresIn
     * @return true-已过期,false-没有过期
     */
    public static boolean isExpiresIn(long expiresIn){
        long now=System.currentTimeMillis();
        return now>expiresIn;
    }
}

自定义一个认证成功的处理类,当SpringSecurity认证通过后调用的方法,需要在配置类中进行配置

java 复制代码
import com.alibaba.fastjson.JSON;
import com.tuling.jwtdemo.utils.JWTUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    private String secret = "123456xxxx"; //秘钥

    private long expMillis = 3600000 ;//30分钟过期,可根据实际情况自行修改;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        Object principal = authentication.getPrincipal();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();

        User user= (User) principal;
        //1.从authentication 取出用户信息,保存到claims map对象
        Map<String, Object> claims=new HashMap<>();
        claims.put("username",user.getUsername());
        claims.put("authorities",user.getAuthorities());
        claims.put("enabled",user.isEnabled());
        claims.put("expiresIn",(System.currentTimeMillis()+expMillis));
        //2.生成token
        String token = JWTUtils.getAccessToken(secret, claims);

        Map<String,Object>result=new HashMap<>();
        result.put("accessToken",token);
        //3.将token以JSON串返回前端
        out.write(JSON.toJSONString(result));
        out.flush();
        out.close();
    }
}

创建一个SpringSecurity的配置类,主要功能是:

  • .successHandler(loginSuccessHandler) 指定认证成功之后的处理handler类
  • http.addFilterBefore(..) 添加JWT登录拦截器,在登录之前获取token并校验
java 复制代码
import com.tuling.jwtdemo.filter.JwtAuthenticationTokenFilter;
import com.tuling.jwtdemo.handler.*;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;

import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;


@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    // 登录成功之后处理类
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    // 登录失败的处理类,与当前业务关系不大 就不贴代码了
    @Autowired
    private LoginFailureHandler loginFailureHandler;

    // 下面两个是认证和授权失败后自定义处理类,与当前业务关系不大 就不贴代码了
    @Autowired
    private LoginAuthenticationEntryPoint loginAuthenticationEntryPoint;
    @Autowired
    private BussinessAccessDeniedHandler bussinessAccessDeniedHandler;

    // 校验JWTtoken的Filter,在下面会有具体代码
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    // 登出相关的handler,与当前业务关系不大 就不贴代码了
    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        //对请求进行访问控制设置
        http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                //设置哪些路径可以直接访问,不需要认证;哪些路径需要什么什么权限
                .requestMatchers("/login").permitAll()
                .requestMatchers("/user/**").hasRole("admin")
                .anyRequest().authenticated() //其他路径的请求都需要认证
        );

        //自定义登录逻辑
        http.formLogin((formLogin) -> formLogin
                //登录访问路径:前台界面提交表单之后跳转到这个路径进行UserDetailsService的验证,必须和表单提交接口一样  
                // 并且提交的用户名和密码请求字段名必须为username、password;在UsernamePasswordAuthenticationFilter类中写死了
                .loginProcessingUrl("/login")
                // 认证成功之后的处理handler类,也就是上面我们自己定义的类
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
        );

        //添加JWT登录拦截器,在登录之前获取token并校验
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


        //访问受限后的异常处理
        http.exceptionHandling((exceptionHandling) -> exceptionHandling
                .authenticationEntryPoint(loginAuthenticationEntryPoint)
                .accessDeniedHandler(bussinessAccessDeniedHandler)
        );
        //自定义退出登录逻辑
        http.logout((logout) -> logout
                .logoutSuccessHandler(myLogoutSuccessHandler)
        );
        //关闭跨站点请求伪造csrf防护
        http.csrf((csrf) -> csrf.disable());

        return http.build();
    }
}

定义JWT 校验token的Filter,主要功能为:

  • 验证token是否过期
  • 从token中取用户信息
  • 因为当前是整合了SpringSecurity,之后还有认证的Filter去处理,所以这里就直接setAuthentication(null)置为null就行了。需要注意的是,我们必须在config配置类中指定JWT 验证token的filter 在 认证用户名密码的filter之前。http.addFilterBefore(...)
java 复制代码
import com.tuling.jwtdemo.utils.JWTUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.security.core.context.SecurityContextHolder;

import java.io.IOException;
import java.util.Map;

@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //1.从请求头中取出token,进行判断,如果没有携带token,则继续往下走其他的其他的filter逻辑
        String tokenValue = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (!StringUtils.hasText(tokenValue)) {
            filterChain.doFilter(request, response);
            return;
        }
        //2. 校验token
        //2.1 将token切割前缀"bearer ",然后使用封装的JWT工具解析token,得到一个map对象
        String token = tokenValue.substring("bearer ".length());
        Map<String, Object> map = JWTUtils.parseToken(token);
        //2.2 取出token中的过期时间,调用JWT工具中封装的过期时间校验,如果token已经过期,则删除登录的用户,继续往下走其他filter逻辑
        if (JWTUtils.isExpiresIn((long) map.get("expiresIn"))) {
            //token 已经过期
            // 因为当前是整合了SpringSecurity,之后还有认证的Filter去处理,所以这里就直接置为null就行了
            // 当然也可以自定义,比如直接抛自定义的业务异常
            SecurityContextHolder.getContext().setAuthentication(null);
            filterChain.doFilter(request, response);
            return;
        }

        String username = (String) map.get("username");
        if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 调用实现了UserDetailsService接口的Service方法,获取用户信息
            // 当然也可以自己改造,自己写service方法,自己查数据库,自己缓存User信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (userDetails != null && userDetails.isEnabled()) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                log.info("authenticated user {}, setting security context", username);
                // 设置用户登录状态
                // 添加了下面这行代码,之后的认证Filter就不会再对当前请求进行认证了
                SecurityContextHolder.getContext().setAuthentication(authentication);
                // 其实哪怕设置的全都是null,之后的认证Filter也不会对当前请求进行认证
                //SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(null,null,null));
            }
        }
        filterChain.doFilter(request, response);

    }
}

创建一个查询User的service,我这里只是随便写一个UserDetails,正常处理是去查询数据库,然后封装为一个UserDetails对象返回

java 复制代码
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //TODO 从数据库获取用户信息

        return new User("hushang","{noop}123456",
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,ROLE_user"));
    }
}

测试效果

启动应用后调用登录接口返回token信息,必须是发送post请求

重启微服务之后,不带token信息访问接口,返回401,没有权限

带token信息访问接口,返回正常

我们也可以自定义登录认证接口

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {


    http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                               .requestMatchers("/user/**").hasRole("admin")
                               .requestMatchers("/loginXXX").permitAll()  // 对我们自定义的登录接口放行
                               .anyRequest().authenticated() 
                              );

    //自定义登录逻辑
    http.formLogin((formLogin) -> formLogin
                   .loginProcessingUrl("/loginXXX")  // 自定义登录接口
                   .successHandler(loginSuccessHandler)
                   .failureHandler(loginFailureHandler)
                  );

    //......
}

测试:

JWT续期问题

JWT通常是在用户登录后签发的,用于验证用户身份和授权。

JWT 的有效期限(或称"过期时间")通常是一段时间(例如1小时),过期后用户需要重新登录以获取新的JWT。然而,在某些情况下,用户可能会在JWT到期之前使用应用程序,这可能会导致应用程序不可用或需要用户重新登录。

JWT的续期都需要重新生成token,区别就是每一次请求都生成新token,或者是仅仅对即将过期/已经过期生成新token

刷新令牌(Refresh Token)

  1. 校验当前token是否有效
  2. 从当前token中取用户信息
  3. 根据用户信息重新生成一个新token返回

这种方式仅仅只是不需要用户重新登录,但是每次请求都会生成新token,都需要前端来保存新的token,覆盖老的token

伪代码如下

java 复制代码
// 方法接收一个刷新令牌作为参数
public String refreshAccessToken(String refreshToken) {
 
    // 验证该令牌是否有效
    boolean isValid = validateRefreshToken(refreshToken);
 
    if (isValid) {
        // 获取与令牌关联的用户信息
        String userId = getUserIdFromRefreshToken(refreshToken);
 
        // 生成一个新的JWT访问令牌
        String newAccessToken = generateAccessToken(userId);
 
        return newAccessToken;
    } else {
        throw new RuntimeException("Invalid refresh token.");
    }
}

自动延长JWT有效期

在某些情况下,JWT可以自动延长其有效期。例如,当用户在JWT过期前继续使用应用程序时,应用重新设置token过期时间。

要自动延长JWT有效期,您可以在每次请求时检查JWT的过期时间,并在必要时更新JWT的过期时间。

具体的思路就是:每次请求检查token是否过期,如果过期了则获取用户信息重新生成一个token返回,如果没有过期但即将过期,也是生成一个新token返回

java 复制代码
public String getAccessToken(HttpServletRequest request) {
 
    // 从请求中提取JWT访问令牌
    String accessToken = extractAccessTokenFromRequest(request);
 
    // 检查JWT的过期时间是否已过期
    if (isAccessTokenExpired(accessToken)) {
        // 过期
        // 从token中获取userid,在重新生成一个token
        String userId = extractUserIdFromAccessToken(accessToken);
        accessToken = generateNewAccessToken(userId);
        
        // 没有过期,检查是不是即将过期
    } else if (shouldRefreshAccessToken(accessToken)) {
        String userId = extractUserIdFromAccessToken(accessToken);
        accessToken = generateNewAccessToken(userId);
    }
 
    return accessToken;
}
 
private boolean isAccessTokenExpired(String accessToken) {
    // 提取过期时间
    Date expirationTime = extractExpirationTimeFromAccessToken(accessToken);
 
    // 过期时间是否在当前时间之前,其实通俗理解就是如果过期了就返回true
    return expirationTime.before(new Date());
}
 
private boolean shouldRefreshAccessToken(String accessToken) {
    // 提取过期时间
    Date expirationTime = extractExpirationTimeFromAccessToken(accessToken);
    Date currentTime = new Date();
 
    // 距离过期的剩余时间
    long remainingTime = expirationTime.getTime() - currentTime.getTime();
 
    // 如果令牌在接下来的5分钟内到期,则需要刷新令牌
    return remainingTime < 5 * 60 * 1000;
}
 
private String generateNewAccessToken(String userId) {
    // 重新生成JWT token
    Date expirationTime = new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME);
    String accessToken = generateAccessToken(userId, expirationTime);
 
    return accessToken;
}

SpringSecurity总结

我们使用SpringSecurity主要就是做认证和授权

认证

  • 我们先自定义一个认证成功与认证失败的处理handler类,我们会在认证成功handler中生成token返回给前端

  • 在config配置类中指定要登录认证的接口路径,并指定认证成功与认证失败的处理handler类

    java 复制代码
    http.formLogin((formLogin) -> formLogin
                    .loginProcessingUrl("/login")
                    .successHandler(loginSuccessHandler)
                    .failureHandler(loginFailureHandler)
            );
  • 前端调用登录认证接口时,用户名密码必须是username和password,这是在UsernamePasswordAuthenticationFilter默认值,更改更改需要我们做相应的配置

  • 自定义一个Filter,必须要在认证用户名密码filter之前执行;该Filter校验token,验证通过就跳过后续的认证Filter

    java 复制代码
    //添加JWT登录拦截器,在登录之前获取token并校验
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    
    
    // 在Filter验证token通过之后,需要在最后添加下面这条语句,用以跳过UsernamePasswordAuthenticationFilter的认证流程
    SecurityContextHolder.getContext().setAuthentication(...)

鉴权

  • 自定义查询UserService类,实现UserDetailsService接口,在重写的抽象方法中利用username去查询DB,将权限信息一起封装成UserDetails对象返回

    java 复制代码
    UserDetailsServiceImpl implements UserDetailsService
  • 在config配置类中或者使用注解的方式,定义接口需要的权限

    java 复制代码
    http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                               //设置哪些路径可以直接访问,不需要认证
                               .requestMatchers("/login").permitAll()
                               .requestMatchers("/user/**").hasRole("admin")
                               .anyRequest().authenticated() //其他路径的请求都需要认证
                              );
  • 当有请求时,会在AuthorizationFilter中对该请求进行鉴权

相关推荐
用户83071968408210 小时前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解11 小时前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解11 小时前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记15 小时前
Spring Boot Web MVC配置详解
spring boot·后端
用户962377954481 天前
DVWA 靶场实验报告 (High Level)
安全
初次攀爬者1 天前
Kafka 基础介绍
spring boot·kafka·消息队列
数据智能老司机1 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
用户8307196840821 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
数据智能老司机1 天前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
Java水解1 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端