Spring Security 6.0 - Token下發(fā)

前言

幾年前寫(xiě)過(guò)一篇 Spring Security 相關(guān)博文:Spring Boot - 集成 Spring Security,基于 5.0 版本。而當(dāng)前 Spring Security 最新穩(wěn)定版本為 Spring Security 6.2.0,相較于 5.0 版本,6.0 版本的 Spring Security 引入了很多破壞性更新,比如對(duì)一些類進(jìn)行了移除,方法重命名,采用DSL配置,廢棄了一些方法...,因此,那篇博文中的很多配置已不能生效了。

Token下發(fā)

當(dāng)前 Spring Security 最新穩(wěn)定版本為 Spring Security 6.2.0,相較于 5.0 版本,6.0 版本的 Spring Security 引入了很多破壞性更新,比如對(duì)一些類進(jìn)行了移除,方法重命名,采用DSL配置,廢棄了一些方法...,因此,本文中的很多配置已不能生效了。

這里采用最新 Spring Secuirty 6+,對(duì) Token下發(fā) 給出最新示例配置。

前文介紹過(guò),Spring Security 默認(rèn)登錄接口為/login,默認(rèn)是由UsernamePasswordAuthenticationFilter進(jìn)行表單登錄認(rèn)證,我們前面也是通過(guò)自定義UsernamePasswordAuthenticationFilter實(shí)現(xiàn) JSON登錄認(rèn)證。不過(guò),此處我們進(jìn)行簡(jiǎn)化,不再用 Spring Security 默認(rèn)的登錄接口和邏輯,而是通過(guò)自定義注冊(cè)和登錄接口(/signup & /signin)實(shí)現(xiàn)登錄認(rèn)證,然后通過(guò)自定義一個(gè) JSON Web Token 過(guò)濾器(JwtTokenAuthenticationFilter),進(jìn)行 Token 驗(yàn)證,實(shí)現(xiàn)用戶認(rèn)證。具體步驟如下:

  1. 前期配置:在正式進(jìn)行 Sprng Security 配置前,先將前期環(huán)境配置一下,包含下面幾方面:

    • 測(cè)試接口:添加測(cè)試接口,模擬真實(shí)業(yè)務(wù)接口:

      @RestController
      @RequestMapping
      public class TestApi {
      
          // 測(cè)試
          @GetMapping("/test")
          public String index() {
              return "hello world!";
          }
      
          // 獲取當(dāng)前用戶信息
          @GetMapping("/user")
          public String whoami() {
              Authentication auth = SecurityContextHolder.getContext().getAuthentication();
              String name = auth.getName();
              Object principal = auth.getPrincipal();
              String password = (String) auth.getCredentials();
              Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
              HttpServletRequest request = (HttpServletRequest) auth.getDetails();
      
              StringBuilder builder = new StringBuilder();
              builder.append(String.format("name: %s\n", name));
              builder.append(String.format("principal: %s\n", principal));
              builder.append(String.format("password: %s\n", password));
              builder.append(String.format("authorities: %s\n", authorities.stream()
                      .map(GrantedAuthority::getAuthority)
                      .collect(Collectors.joining(",")))
              );
              builder.append(String.format("getDetails: %s\n", request));
              return builder.toString();
          }
      }
      
    • 用戶數(shù)據(jù)庫(kù):這里我們使用真實(shí)的數(shù)據(jù)庫(kù),用戶表如下所示:

      -- create tbale tb_user
      create table `tb_user` (
          `id` bigint unique not null auto_increment,
          `name` varchar(30) unique not null comment 'user name',
          `password` varchar(100) not null comment 'user password',
          `role` enum('admin','normal','anonymous') default 'anonymous' comment 'user role',
          `authority` set('create','read','update','delete') comment 'user authorities',
      
          primary key(`id`)
      );
      
    • 數(shù)據(jù)庫(kù)相關(guān)配置

      • 導(dǎo)入相關(guān)依賴:
        <!-- pom.xml -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        
      • 設(shè)置相關(guān)配置:
        spring:
          datasource:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://127.0.0.1:3306/whyn
            username: root
            password: 123456
        
        mybatis:
          mapper-locations: classpath:mapper/**/*.xml
        
    • 用戶表操作:采用 MyBatis

      • 實(shí)體類:這里實(shí)體類User實(shí)現(xiàn)了 Spring Security 的UserDetails,表示用戶信息:

        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        @Builder
        public class User implements UserDetails {
            private Long id;
            private String name;
            private String password;
            private String role;
            private String authority;
        
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                String[] authorities = this.authority.split(",");
                String rolePrefix = "ROLE_";
                String roleAuthority = this.role;
                // 將 role 轉(zhuǎn)成 ROLE_XXX
                if (null != roleAuthority && !roleAuthority.startsWith(rolePrefix)) {
                    roleAuthority = (rolePrefix + roleAuthority).toUpperCase();
                }
        
                return Stream.concat(
                                Arrays.stream(authorities),
                                Stream.of(roleAuthority)
                        ).filter(Predicate.not(String::isBlank))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
            }
        
            // UserDetailsService#loadUserByUsername(username)
            // 其中的 username 就是 getUsername,本質(zhì)是一個(gè)唯一的標(biāo)識(shí),此處可使用其他唯一性字段進(jìn)行代替
            @Override
            public String getUsername() {
                return this.name;
            }
        
            @Override
            public String getPassword() {
                return this.password;
            }
        
        
            @Override
            public boolean isAccountNonExpired() {
                return true;
            }
        
            @Override
            public boolean isAccountNonLocked() {
                return true;
            }
        
            @Override
            public boolean isCredentialsNonExpired() {
                return true;
            }
        
            @Override
            public boolean isEnabled() {
                return true;
            }
        }
        
      • 用戶表操作類:

        @Mapper
        public interface IUserDao {
            @Insert("insert into tb_user values( #{ id }, #{ name }, #{ password }, #{ role }, #{ authority })")
            int insert(User user);
            @Select("select * from tb_user where id = #{id}")
            User selectOneByPrimaryKey(Long id);
            @Select("select * from tb_user where name=#{ name }")
            User selectOneByName(String name);
        }
        
      • 用戶表服務(wù)類:

        // IUserService.java
        public interface IUserService {
            User findUser(String name);
        }
        
        // UserServiceImpl.java
        @Service
        @AllArgsConstructor
        public class UserServiceImpl implements IUserService {
            private final IUserDao userDao;
            @Override
            public User findUser(String name) {
                return this.userDao.selectOneByName(name);
            }
        }
        

    至此,前期準(zhǔn)備工作已完成,可以開(kāi)始配置 Spring Security 相關(guān)內(nèi)容。

  2. 引入相關(guān)依賴

    <!-- 導(dǎo)入 Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <!-- 導(dǎo)入 jjwt -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.3</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.3</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
        <version>0.12.3</version>
        <scope>runtime</scope>
    </dependency>
    
  3. JWT工具類:配置過(guò)程如下:

    • 配置 JWT 相關(guān)信息:

      # application.yml
      jwt:
        token:
          # 密鑰
          secret-key: "this_is_private_secret_key"
          # 過(guò)期時(shí)間 7 天
          # jshell> TimeUnit.DAYS.toMillis(7);
          # $1 ==> 604800000
          expiration: 604800000
          # token 前綴
          token-prefix: Bearer
      
    • 抽取一個(gè) JWT 工具類:

      @Service
      public class JwtTokenService {
          public static final String KEY_USER_NAME = "username";
          public static final String KEY_USER_AUTHORITIES = "authorities";
      
          @Value("${jwt.token.secret-key}")
          private String secretKey;
          @Value("${jwt.token.expiration}")
          private long expiration;
          @Value("${jwt.token.token-prefix}")
          private String tokenPrefix;
      
          public String getTokenPrefix() {
              return this.tokenPrefix;
          }
      
          // 解析 token
          public Map<String, Object> parseToken(String token) {
              Map<String, Object> userDetails = new HashMap<>();
              try {
                  token = validateToken(token);
                  Jws<Claims> claimsJws = Jwts.parser()
                          .setSigningKey(this.getSecretKey()).build().parseClaimsJws(token);
                  // 用戶名
                  String username = claimsJws.getBody().getSubject();
                  userDetails.put(KEY_USER_NAME, username);
                  // 用戶權(quán)限
                  List<Map<String, String>> authorities = (List<Map<String, String>>) claimsJws.getBody().get(KEY_USER_AUTHORITIES);
                  if (null != authorities) {
                      Collection<? extends GrantedAuthority> userAuthorities = authorities.stream()
                              .map(item ->
                                      new SimpleGrantedAuthority(item.get("authority")))
                              .collect(Collectors.toSet());
                      userDetails.put(KEY_USER_AUTHORITIES, userAuthorities);
                  }
                  return userDetails;
              } catch (JwtException e) {
                  throw new IllegalStateException(String.format("invalid token: %s", token));
              }
          }
      
          // 生成token
          public String generateToken(String username) {
              String token = Jwts.builder()
                      .setSubject(username)
                      .setIssuedAt(new Date())
                      .setExpiration(new Date(System.currentTimeMillis() + this.expiration))
                      .signWith(this.getSecretKey())
                      .compact();
              return generateTokenWithPrefix(token);
          }
      
          // 生成 token,包含用戶主體和其權(quán)限
          public String generateToken(String username, Object authorities) {
              String token = Jwts.builder()
                      // 用戶名
                      .setSubject(username)
                      // payload
                      .claim(KEY_USER_AUTHORITIES, authorities)
                      // 發(fā)行時(shí)間
                      .setIssuedAt(new Date())
                      // 過(guò)期時(shí)間
                      .setExpiration(new Date(System.currentTimeMillis() + this.expiration))
                      // 私鑰
                      .signWith(getSecretKey())
                      .compact();
              return generateTokenWithPrefix(token);
          }
      
          // token 添加前綴 Bearer
          private String generateTokenWithPrefix(final String token) {
              return String.format("%s %s", this.tokenPrefix, token);
          }
      
          // 生成簽名私鑰
          private Key getSecretKey() {
              return Keys.hmacShaKeyFor(generateSecretKey());
          }
      
          // 加密要求至少 256 位,因此將私鑰進(jìn)行 sha256,只是單純?yōu)榱松?256 個(gè)字節(jié)
          private byte[] generateSecretKey() {
              byte[] hashKey = null;
              String secretKey = this.secretKey;
              try {
                  MessageDigest digest = MessageDigest.getInstance("SHA-256");
                  hashKey = digest.digest(secretKey.getBytes(StandardCharsets.UTF_8));
              } catch (NoSuchAlgorithmException e) {
                  hashKey = this.fillBytes(secretKey);
              }
              return hashKey;
          }
      
          // 循環(huán)字符串添加到 256 個(gè)字節(jié)
          private byte[] fillBytes(String str) {
              if (str == null) {
                  throw new IllegalArgumentException("secret key must not be null!");
              }
              byte[] bytes256 = new byte[256];
              int length = str.length();
              for (int i = 0; i < 256; ++i) {
                  // 忽視精度缺失,只是為了添加到 256 個(gè)字節(jié)
                  bytes256[i] = (byte) str.charAt(i % length);
              }
              return bytes256;
          }
      
          // 去除 token 前綴:Bearer
          private String validateToken(String token) {
              String rawToken = token;
              String tokenPrefix = this.tokenPrefix;
              if (rawToken.startsWith(tokenPrefix)) {
                  rawToken = rawToken.substring(tokenPrefix.length()).trim();
              }
              return rawToken;
          }
      }
      
  4. 用戶登錄認(rèn)證配置:Spring Security 對(duì)于用戶登錄有一套完整的認(rèn)證流程,比如我們常見(jiàn)的表單登錄認(rèn)證,它是由UsernamePasswordAuthenticationFilter負(fù)責(zé)處理的,整個(gè)認(rèn)證流程如下圖所示:

    spring-security-authentication-process

    :圖片來(lái)源于互聯(lián)網(wǎng),侵刪

    簡(jiǎn)單來(lái)說(shuō),認(rèn)證過(guò)程是通過(guò)AuthenticationManager#authenticate開(kāi)啟認(rèn)證,然后經(jīng)由AuthenticationProvider#authenticate,然后從與其綁定的UserDetailsService#loadUserByUsername獲取到真實(shí)的用戶信息UserDetails,如此,AuthenticationProvider就可以比對(duì)前端傳遞過(guò)來(lái)的用戶密碼與數(shù)據(jù)庫(kù)中該用戶密碼是否匹配,匹配則驗(yàn)證成功,最后會(huì)構(gòu)建一個(gè)新的Authentication對(duì)象,保存用戶相關(guān)信息,并放置到SecurityContextHolder的上下中,供后續(xù)組件獲取該用戶信息。

    因此,對(duì)于用戶認(rèn)證流程,我們這里需要配置以上相關(guān)組件,如下所示:

    @Configuration
    @EnableWebSecurity
    @AllArgsConstructor
    public class SecurityConfiguration {
    
        private final IUserService userService;
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            // 手動(dòng)關(guān)聯(lián)我們?cè)O(shè)置的 AuthenticationManager(可選,默認(rèn)注冊(cè)已關(guān)聯(lián))
            http.authenticationManager(this.authenticationManager());
            return http.build();
        }
    
        // 密碼加密器
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        // 獲取用戶及其相關(guān)詳細(xì)信息
        @Bean
        public UserDetailsService userDetailsService() {
            return new UserDetailsService() {
                @Override
                public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                    User user = userService.findUser(username);
                    if (null == user) {
                        throw new UsernameNotFoundException(String.format("[username: %s] not found!", username));
                    }
                    return user;
                }
            };
        }
    
        // 負(fù)責(zé)具體用戶認(rèn)證流程
        @Bean
        public AuthenticationProvider authenticationProvider() {
            // 創(chuàng)建一個(gè)用戶認(rèn)證提供者
            DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
            // 將該 Provider 關(guān)聯(lián)到我們?cè)O(shè)置的 UserDetailsService
            authProvider.setUserDetailsService(this.userDetailsService());
            // 關(guān)聯(lián)到我們?cè)O(shè)置的加密算法
            authProvider.setPasswordEncoder(this.passwordEncoder());
            return authProvider;
        }
    
        // 認(rèn)證管理者
        @Bean
        public AuthenticationManager authenticationManager() {
            // 關(guān)聯(lián)到我們?cè)O(shè)置的 AuthenticationProvider
            return new ProviderManager(this.authenticationProvider());
        } 
    }
    
  5. 自定義注冊(cè)和登錄接口

    • Controller

      @RestController
      @RequestMapping("/auth")
      public class AuthApi {
      
          @Autowired
          private IAuthService authService;
      
          // 用戶注冊(cè)接口
          @PostMapping("/signup")
          public boolean signUp(@RequestBody User user) {
              return this.authService.signUp(user);
          }
      
          // 用戶登錄接口
          @PostMapping("/signin")
          public void signIn(@RequestBody User user, HttpServletResponse response) {
              String jwtToken = this.authService.signIn(user);
              Optional.ofNullable(jwtToken)
                      .ifPresentOrElse(token -> {
                          this.success(response, token);
                      }, () -> {
                          this.failed(response);
                      });
          }
      
          private void success(HttpServletResponse response, String jwtToken) {
              response.addHeader(HttpHeaders.AUTHORIZATION, jwtToken);
              response.setCharacterEncoding("utf-8");
              response.setContentType("application/json");
              try (PrintWriter writer = response.getWriter()) {
                  writer.print("login successfully!");
              } catch (IOException e) {
                  throw new RuntimeException(e);
              }
          }
      
          private void failed(HttpServletResponse response) {
              response.setStatus(HttpStatus.UNAUTHORIZED.value());
              try (PrintWriter writer = response.getWriter()) {
                  writer.print("login failed! username or password incorrect");
              } catch (IOException e) {
                  throw new RuntimeException(e);
              }
      
          }
      }
      
    • Service:

      // IAuthService.java
      public interface IAuthService {
      
          boolean signUp(User user);
      
          String signIn(User user);
      }
      
      // AuthServiceImpl.java
      @Service
      @AllArgsConstructor
      public class AuthServiceImpl implements IAuthService {
      
          private final IUserDao userDao;
          private final JwtTokenService jwtTokenService;
          private final AuthenticationManager authManager;
          private final PasswordEncoder passwordEncoder;
      
          @Override
          public boolean signUp(User user) {
              String encodePassword = this.passwordEncoder.encode(user.getPassword());
              user.setPassword(encodePassword);
              return this.userDao.insert(user) > 0;
          }
      
          @Override
          public String signIn(User user) {
              String jwtToken = null;
              try {
                  String username = user.getUsername();
                  String password = user.getPassword();
                  if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
                      throw new AuthenticationServiceException("username & password must not be null");
                  }
                  // 構(gòu)造一個(gè) Authentication 對(duì)象,設(shè)置 principal & credentials
                  // principal 意為主要的,對(duì)應(yīng)數(shù)據(jù)庫(kù)中唯一字段(unique key),此處設(shè)置為 username,
                  // 若進(jìn)行更改,則相應(yīng)的 userDetails#getUsername() 和 UserDetailsService#loadUserByUsername(String username)
                  // 都要設(shè)置為對(duì)同一字段進(jìn)行操作
                  // credentials 就是指代密碼
                  Authentication authentication = new UsernamePasswordAuthenticationToken(username, password);
                  // 可自定義填充其余信息,方便后續(xù)獲取用戶時(shí),能獲取這些自定義信息
                  // ((UsernamePasswordAuthenticationToken)authentication).setDetails(null);
      
                  // AuthenticationManager 進(jìn)行認(rèn)證,認(rèn)證失敗拋異常
                  // 認(rèn)證通過(guò),成功返回一個(gè)新的 Authentication 對(duì)象,其內(nèi)包含有用戶所有信息(包含上面自定義信息 setDetails),只是將密碼去除
                  Authentication successDetailedAuth = this.authManager.authenticate(authentication);
                  // 認(rèn)證通過(guò)
                  // 獲取用戶詳細(xì)信息
                  Collection<? extends GrantedAuthority> authorities = successDetailedAuth.getAuthorities();
                  // 下發(fā) jwt token
                  jwtToken = this.jwtTokenService.generateToken(username, authorities);
      
              } catch (AuthenticationException e) {
                  e.printStackTrace();
              }
              return jwtToken;
          }
      }
      

    至此,登錄和注冊(cè)功能就完成了。每次登錄時(shí),成功后會(huì)在響應(yīng)頭中攜帶上一串 Jwt Token,后續(xù)請(qǐng)求都需要攜帶該 token,后端對(duì)該 token 驗(yàn)證成功,才允許其訪問(wèn)相應(yīng)資源。

  6. 設(shè)置 Jwt Token 驗(yàn)證過(guò)濾器

    @AllArgsConstructor
    public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
    
        private final JwtTokenService jwtTokenService;
        private final UserDetailsService userDetailsService;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain filterChain) throws ServletException, IOException {
            try {
                String jwtToken = request.getHeader(HttpHeaders.AUTHORIZATION);
                if (null != jwtToken && jwtToken.startsWith(this.jwtTokenService.getTokenPrefix())) {
                    // 解析 jwt token,失敗拋異常
                    Map<String, Object> userDetailsMap = this.jwtTokenService.parseToken(jwtToken);
                    // 認(rèn)證通過(guò),從 token 中提取出 username
                    String username = (String) userDetailsMap.get(JwtTokenService.KEY_USER_NAME);
                    // 獲取用戶詳細(xì)信息
                    UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                    // 構(gòu)建一個(gè) Authentication 認(rèn)證對(duì)象,填入用戶相關(guān)信息
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                            username,     // principal 用戶名
                            null, // credentials 密碼敏感數(shù)據(jù),直接置為空即可
                            userDetails.getAuthorities());
                    // 附加詳細(xì)信息,比如請(qǐng)求體,有些認(rèn)證方式需要除了用戶名密碼外更多的信息
                    // 后續(xù)可通過(guò) Authentication#getDetails() 獲取自定義的額外信息
                    authentication.setDetails(request);
                    // 認(rèn)證成功,直接設(shè)置到 SecurityContextHolder 中,供后續(xù) Filters 使用
                    // 該操作會(huì)將 Authentication 存放到 ThreadLocal 中,這樣當(dāng)前請(qǐng)求在后續(xù)操作中就能獲取到該 Authentication
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            filterChain.doFilter(request, response);
        }
    }
    

    當(dāng) Jwt token 解析成功時(shí),則表示驗(yàn)證通過(guò),最后會(huì)將當(dāng)前用戶相關(guān)信息包裹在一個(gè)Authentication對(duì)象中,并將該對(duì)象放置在全局SecurityContextHolder中,方便后續(xù)組件獲取當(dāng)前用戶信息。

  7. 配置一個(gè) SecurityFilterChain:配置一個(gè)SecurityFilterChain,關(guān)聯(lián)我們自定義的JwtTokenAuthenticationFilter,讓其生效;同時(shí)配置其他相關(guān)信息:

    @Configuration
    @EnableWebSecurity
    @AllArgsConstructor
    public class SecurityConfiguration {
    
        private final IUserService userService;
        private final JwtTokenService jwtTokenService;
    
        // 
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.csrf(AbstractHttpConfigurer::disable)
                    .cors(Customizer.withDefaults())
                    .sessionManagement(sessionManager -> sessionManager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .authenticationManager(this.authenticationManager())
                    // 配置 JwtTokenAuthenticationFilter
                    .addFilterBefore(new JwtTokenAuthenticationFilter(
                                    this.jwtTokenService,
                                    this.userDetailsService()),
                            UsernamePasswordAuthenticationFilter.class)
                    // 請(qǐng)求認(rèn)證授權(quán)
                    .authorizeHttpRequests(requests -> {
                        // 放行 POST 請(qǐng)求接口 /auth/signin,/auth/signup
                        requests.requestMatchers(HttpMethod.POST, "/auth/signin", "/auth/signup").permitAll()
                                // 放行 /test/** 接口所有請(qǐng)求
                                .requestMatchers("/test/**").permitAll()
                                // 其余請(qǐng)求,一律需要進(jìn)行認(rèn)證
                                .anyRequest().authenticated();
                    });
    
            return http.build();
    
        }
        // ...
    }
    

以上,我們便完成了 Spring Security 6+ 版本對(duì) Token下發(fā) 功能的一個(gè)配置。

我們可以模擬一個(gè)用戶注冊(cè),登錄,訪問(wèn)完整邏輯,測(cè)試如下:

# 注冊(cè)用戶:admin
$ curl -X POST 'localhost:8080/auth/signup' --header 'Content-Type: application/json; charset=utf-8' --data '{"name":"admin", "password":"admin_password", "role": "admin", "authority": "create,read,update,delete"}' 
true% 

# 登錄用戶:admin
$ curl -X POST 'localhost:8080/auth/signin' --header 'Content-Type: application/json; charset=utf-8' --data '{"name":"admin", "password":"admin_password"}' -v
# 可以看到,登錄成功后會(huì)返回一個(gè) Jwt token
< Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGhvcml0aWVzIjpbeyJhdXRob3JpdHkiOiJjcmVhdGUifSx7ImF1dGhvcml0eSI6InJlYWQifSx7ImF1dGhvcml0eSI6InVwZGF0ZSJ9LHsiYXV0aG9yaXR5IjoiZGVsZXRlIn0seyJhdXRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpYXQiOjE3MDI2MzQ3MTEsImV4cCI6MTcwMzIzOTUxMX0.RpyqdBlmbfWsLR1M6mb7SH9RPpFgJJODiZ1mIvx8T5Y
login successfully!% 

# 訪問(wèn)資源
 $ curl -X GET 'localhost:8080/user' --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGhvcml0aWVzIjpbeyJhdXRob3JpdHkiOiJjcmVhdGUifSx7ImF1dGhvcml0eSI6InJlYWQifSx7ImF1dGhvcml0eSI6InVwZGF0ZSJ9LHsiYXV0aG9yaXR5IjoiZGVsZXRlIn0seyJhdXRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpYXQiOjE3MDI2MzQ3MTEsImV4cCI6MTcwMzIzOTUxMX0.RpyqdBlmbfWsLR1M6mb7SH9RPpFgJJODiZ1mIvx8T5Y'
name: admin
principal: admin
password: null
authorities: create,read,update,delete,ROLE_ADMIN
getDetails: org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@60e25235

完整源碼可查看:spring-security-demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容