天天看點

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

文章目錄

  • 前言
  • 一、Spring Security是什麼?
    • 1.認證
      • 1.1基于Session認證
      • 1.2基于Token認證
    • 2.授權
    • 3.基于角色通路控制
  • 二、OAuth2.0是什麼?
  • 三、JWT是什麼?
  • 四、代碼實作
    • 1.建立Spring Boot項目
    • 2.Spring Security
      • 2.1基本使用
      • 2.2自定義功能
        • 2.2.1內建資料庫
      • 2.3通過注解權限控制
    • 3.OAuth2.0
      • 3.1基本使用
      • 3.2四種授權模式
        • 3.2.1 code碼授權
        • 3.2.2 靜默授權
        • 3.2.3 用戶端授權
        • 3.2.4 密碼授權
    • 4.JWT
      • 4.1 基本使用
        • 4.1.1 token過濾器
        • 4.1.2 WebSecurityConfig
        • 4.1.3 AuthorizationJwtServerConfig
        • 4.1.4 登出

前言

基于Spring Boot項目使用Spring Security+OAuth2.0+JWT搭建使用者認證中心。

一、Spring Security是什麼?

Spring Security是一個強大且高度可定制的身份驗證和通路控制架構。它是用于保護基于Spring的應用程式的實際标準。Spring Security官網

1.認證

1.1基于Session認證

使用者登入成功後,會建立一個session儲存在伺服器端,session id儲存在cookie中

1.2基于Token認證

使用token作為唯一辨別

2.授權

3.基于角色通路控制

二、OAuth2.0是什麼?

OAuth 2.0 是授權的行業标準協定。OAuth 2.0 側重于用戶端開發人員的簡單性,同時為 Web 應用程式、桌面應用程式、行動電話和客廳裝置提供特定的授權流。該規範及其擴充正在IETF非授權工作組内制定。

三、JWT是什麼?

JSON Web Token (JWT) 是一個開放标準(RFC 7519),它定義了一種緊湊且自成一體的方式,将各方之間安全傳輸資訊作為 JSON 對象。此資訊可以進行驗證和信任,因為它是以數字方式簽名的。JWTs 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。

四、代碼實作

1.建立Spring Boot項目

建立一個Spring Boot項目然後通過maven引入所需依賴就可以。需要注意的是不同版本的Spring Boot和依賴包可能會出現方法過時等問題。本demo的Spring Boot的版本是2.4.4。下面是所有依賴包。

<!-- 核心依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- 測試依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

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

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

<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>

<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

<!-- 資料源驅動包 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.4</version>
</dependency>

<!-- hutool -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.5.8</version>
</dependency>

<!-- jwt -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.1.1.RELEASE</version>
</dependency>

<!-- jwt工具 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.3.0</version>
</dependency>

<!--Spring boot Redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 日志 -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

<!-- oauth2.0 -->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.6.RELEASE</version>
</dependency>
           

2.Spring Security

2.1基本使用

在引入Spring Security依賴之後啟動項目,通路http://localhost:8080/login,Spring Security自帶了一個登陸頁面。如圖

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

這就說明Spring Security使用成功。使用者名預設是user,密碼在啟動項目時會顯示在控制台。

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

接下來我們對Spring Security進行改造,實作一些我們自己的需求。

2.2自定義功能

2.2.1內建資料庫

資料庫連接配接省略

1.建立配置類

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 開啟方法級别安全
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
	
	/**
     * 一些簡單的配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用session
        http.sessionManagement().disable();
        http.csrf().disable();
        http.formLogin()
                // 自定義登入成功後路徑
                .defaultSuccessUrl("/hello")
                // 自定義登入路徑
                .loginProcessingUrl("/doLogin");
        // 可以匿名通路
        http.authorizeRequests()
                .antMatchers("/doLogin").anonymous()
                .anyRequest().authenticated();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用者資料從資料庫擷取
        auth.userDetailsService(userDetailsService);
    }
}
           

2.自定義身份認證,建立一個類實作UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    	/* 使用者在登入時,會進入此方法(在配置類中進行配置),參數是使用者名,這裡使用了mybatisplus
    	 * 做了一個簡單的通過使用者名查詢使用者,springsecurity會自動對密碼進行比對
    	 */
        QueryWrapper<com.springboot.demo.security.entity.User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", s);
        com.springboot.demo.security.entity.User sysUser = userMapper.selectOne(wrapper);
        String password = sysUser.getPassword();
        List<GrantedAuthority> userList = new ArrayList<>();
        // userList是權限集合,這裡也是做一個簡單的權限添加
        userList.add(new SimpleGrantedAuthority("add"));
        // springsecurity5.0後密碼需要加密一次,不然會報錯
        return new User("user", bCryptPasswordEncoder.encode(password), userList);
    }
           

3.通路登入頁進行測試

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

登入成功後跳轉到自己配置的登入成功頁面

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

2.3通過注解權限控制

使用注解進行權限控制首先需要在配置類中加上注解@EnableGlobalMethodSecurity(prePostEnabled = true),表示開啟方法級安全級别。有三種機制,這裡使用@PreAuthorize注解,在上述登入中我們把add這個權限配置設定給了使用者,是以登入後直接通路的hello沒有權限限制可以直接通路

@GetMapping(value = "/hello")
public String hello() {
     return "hello";
 }

@PreAuthorize("hasAuthority('add')")
@GetMapping(value = "/add")
public String add() {
    return "add";
}

@PreAuthorize("hasAuthority('del')")
@GetMapping(value = "/del")
public String del() {
   return "del";
}
           

有add的權限也可以通路add

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

通路del就會提示授權異常

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

springsecurity的簡單應用就大功告成。

3.OAuth2.0

3.1基本使用

首先引入所需maven依賴,然後我們建立一個配置類AuthorizationJwtServerConfig內建AuthorizationServerConfigurerAdapter,先使用redis做token存儲。

@Configuration
@EnableAuthorizationServer
public class AuthorizationJwtServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }
	/**
     * 暴露授權服務
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore());
    }
}
           

然後啟動項目,我們可以在控制台中看到列印的内容,說明oauth2.0可以使用。

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

3.2四種授權模式

3.2.1 code碼授權

通過code碼換取token。

在AuthorizationJwtServerConfig配置類添加配置。

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            // 第三方應用用戶端id,相當于賬号,可自定義
            .withClient("web")
            // 第三方應用密碼,需要加密,相當于密碼,可自定義
            .secret(new BCryptPasswordEncoder().encode("web"))
            // 第三方作用域,自定義
            .scopes("read")
            // 授權類型,使用code碼
            .authorizedGrantTypes("authorization_code")
            // 有效時間
            .accessTokenValiditySeconds(7200)
            // 重定向url,必須是公網位址,必須是https
            .redirectUris("https://www.baidu.com");
}
           

重新開機項目後我們在浏覽器上通路http://localhost:8080/oauth/authorize?response_type=code&scope=read&client_id=web&redirect_uri=https://www.baidu.com

response_type參數是授權類型code是code碼授權,scope作用域對應代碼配置中的作用域值,client_id也是對應代碼中的配置,redirect_uri同理。

回車之後會跳轉到登入頁面,然後輸入賬号密碼再次登入,會跳轉到授權頁面

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

确認授權後會跳轉到我們配置的重定向位址,并且獲得了code碼

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

有了code碼之後我們就要用code碼去換取token,使用工具postman發送一個post請求

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

除此之外還需要配置Authorization,類型選擇Basic Auth,賬号密碼是代碼中配置的

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

發送請求之後就會獲得token了

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

3.2.2 靜默授權

直接擷取token,代碼跟code碼授權沒有太大差別,類型換成implicit靜默授權就可以了

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            // 第三方應用用戶端id
            .withClient("app")
            // 第三方應用密碼,需要加密
            .secret(new BCryptPasswordEncoder().encode("app"))
            // 第三方作用域
            .scopes("read")
            // 授權類型
            .authorizedGrantTypes("implicit")
            .accessTokenValiditySeconds(7200)
            .redirectUris("https://www.baidu.com");
}
           

重新開機項目,浏覽器通路http://localhost:8080/oauth/authorize?response_type=token&scope=read&client_id=app&redirect_uri=https://www.baidu.com

注意這裡的response_type的值是token,直接通路也會跳轉到登入頁面,登入成功後進行授權

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

授權成功後,token會顯示在浏覽器上

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

3.2.3 用戶端授權

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            // 第三方應用用戶端id
            .withClient("client")
            // 第三方應用密碼,需要加密
            .secret(new BCryptPasswordEncoder().encode("client"))
            // 第三方作用域
            .scopes("read")
            // 授權類型
            .authorizedGrantTypes("client_credentials")
            .accessTokenValiditySeconds(7200)
            .redirectUris("https://www.baidu.com");
}
           

使用postman發送post請求擷取token,隻需grant_type一個參數

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作
Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

3.2.4 密碼授權

首先更改WebSecurityConfig配置類,增加一個認證管理器

/**
 * 用密碼模式授權認證管理器
 * @return
 * @throws
 */
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}
           

AuthorizationJwtServerConfig類中把AuthenticationManager注入進來,暴露授權服務中加入認證管理器

@Resource
private AuthenticationManager authenticationManager;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            // 第三方應用用戶端id
            .withClient("qq")
            // 第三方應用密碼,需要加密
            .secret(new BCryptPasswordEncoder().encode("qq"))
            // 第三方作用域
            .scopes("read")
            // 授權類型
            .authorizedGrantTypes("password")
            .accessTokenValiditySeconds(7200)
            .redirectUris("https://www.baidu.com");
}

/**
 * 暴露授權服務
 * @param endpoints
 * @throws Exception
 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
}
           

重新開機項目後使用postman來測試,發送post請求

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

Authorization也需要配置

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

成功後直接擷取token

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

4.JWT

4.1 基本使用

使用jwt生成token,并做token的驗證

4.1.1 token過濾器

@Configuration
public class JwtTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String path = httpServletRequest.getRequestURI();
        String method = httpServletRequest.getMethod();
        // 對于登入直接放行
        if ("/login".equals(path) && "POST".equals(method)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        // 擷取token并驗證
        String authorization = httpServletRequest.getHeader("Authorization");
        if (!StrUtil.hasBlank(authorization)) {
            String jwt = authorization.replaceAll("bearer ", "");
            // 建立一個token解析器(test作為jwt生成token的簽名是自定義的,一般是作為配置固定值)
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("test")).build();
            DecodedJWT decodedJwt;
            try {
                decodedJwt = jwtVerifier.verify(jwt);
            } catch (Exception e) {
                httpServletResponse.getWriter().write("token驗證失敗");
                return;
            }
            // 擷取使用者名,密碼,角色權限
            String username = decodedJwt.getClaim("username").asString();
            String password = decodedJwt.getClaim("password").asString();
            List<String> roles = decodedJwt.getClaim("role").asList(String.class);
            List<SimpleGrantedAuthority> roleList = new ArrayList<>();
            roles.forEach(role -> {
                roleList.add(new SimpleGrantedAuthority(role));
            });
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                    new UsernamePasswordAuthenticationToken(username, password, roleList);      
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        httpServletResponse.getWriter().write("token驗證失敗");
    }
}
           

4.1.2 WebSecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @Resource
    private JwtTokenFilter jwtTokenFilter;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 一些簡單的配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 登入之前驗證token
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 禁用session
        http.sessionManagement().disable();
        http.csrf().disable();
        http.formLogin()
                // 登入成功處理器
                .successHandler(authenticationSuccessHandler())
                // 登入失敗處理器
                .failureHandler(authenticationFailureHandler());
        // 除了登入可以匿名通路
        http.authorizeRequests()
                .antMatchers("/login").anonymous()
                .anyRequest().authenticated();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用者資料從資料庫擷取
        auth.userDetailsService(userDetailsService);
    }
	
	/**
	 * 登入成功處理器
	 */
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return ((httpServletRequest, httpServletResponse, authentication) -> {
            httpServletResponse.setContentType("application/json;charset=utf-8");
            User user = (User)authentication.getPrincipal();
            // 使用者名
            String username = user.getUsername();
            // 密碼
            String password = user.getPassword();
            // 權限
            Collection<GrantedAuthority> grantedAuthorities =  user.getAuthorities();
            List<String> roleList = new ArrayList<>();
            grantedAuthorities.forEach(grantedAuthority -> {
                roleList.add(grantedAuthority.getAuthority());
            });
            String[] roles = new String[roleList.size()];
            // 用jwt生成token
            HashMap<String, Object> headMap = new HashMap<>(16);
            // 使用的算法
            headMap.put("alg", "HS256");
            headMap.put("typ", "JWT");
            Date nowDate = new Date();
            // 過期時間可以自定義
            Date expDate = new Date(nowDate.getTime() + 2 * 60 * 60 * 1000);
            String jwt = JWT.create().withHeader(headMap)
                    .withIssuedAt(nowDate)
                    .withExpiresAt(expDate)
                    // 主題,自定義
                    .withSubject("demo")
                    .withClaim("username", username)
                    .withClaim("password", password)
                    .withArrayClaim("role", roleList.toArray(roles))
                    // 簽名,自定義,同一個項目中簽名是唯一
                    .sign(Algorithm.HMAC256("test"));
            // 儲存token到redis
            redisTemplate.opsForValue().set("token:" + jwt, user, 7200);
            // 傳回token
            HashMap<String, Object> hashMap = new HashMap<>(16);
            hashMap.put("username", username);
            hashMap.put("create_time", nowDate);
            hashMap.put("expires_time", expDate);
            hashMap.put("access_token", jwt);
            hashMap.put("type", "bearer");
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(hashMap);
            PrintWriter printWriter = httpServletResponse.getWriter();
            printWriter.write(s);
            printWriter.flush();
            printWriter.close();
        });
    }

	/**
	 * 登入失敗處理器
	 */
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return ((httpServletRequest, httpServletResponse, e) -> {
            HashMap<String, Object> hashMap = new HashMap<>(16);
            hashMap.put("error", e);
            hashMap.put("message", "登入失敗");
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(hashMap);
            PrintWriter printWriter = httpServletResponse.getWriter();
            printWriter.write(s);
            printWriter.flush();
            printWriter.close();
        });
    }
}
           

4.1.3 AuthorizationJwtServerConfig

@Configuration
@EnableAuthorizationServer
public class AuthorizationJwtServerConfig extends AuthorizationServerConfigurerAdapter {

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * jwt token轉換器
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        // 簽名
        jwtAccessTokenConverter.setSigningKey("test");
        return jwtAccessTokenConverter;
    }

    /**
     * 暴露授權服務
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter());
    }
}
           

然後啟動項目,使用postman測試

登入擷取token

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

使用token通路接口

Spring Security + OAuth2.0 + JWT前言一、Spring Security是什麼?二、OAuth2.0是什麼?三、JWT是什麼?四、代碼實作

4.1.4 登出

登出就相當于是删除redis緩存的token

@PostMapping(value = "/doLogout")
    public Object logout() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String head = request.getHeader("Authorization");
        if (!StrUtil.isBlank(head)) {
            String jwt = head.replaceAll("bearer ", "");
            if (!StrUtil.hasBlank(jwt)) {
                redisTemplate.delete("token:" + jwt);
                return "登出成功";
            }
        }
        return "登出失敗";
    }