1. 程式人生 > 程式設計 >SpringBoot整合Spring Security用JWT令牌實現登入和鑑權的方法

SpringBoot整合Spring Security用JWT令牌實現登入和鑑權的方法

最近在做專案的過程中 需要用JWT做登入和鑑權 查了很多資料 都不甚詳細
有的是需要在application.yml裡進行jwt的配置 但我在導包後並沒有相應的配置項 因而並不適用
在踩過很多坑之後 稍微整理了一下 做個筆記

一、概念

1、什麼是JWT

Json Web Token (JWT)是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準(RFC 7519)
該token被設計為緊湊且安全的 特別適用於分散式站點的單點登入(SSO)場景

隨著JWT的出現 使得校驗方式更加簡單便捷化
JWT實際上就是一個字串 它由三部分組成:頭部 載荷和簽名
用[.]分隔這三個部分 最終的格式類似於:xxxx.xxxx.xxxx

在伺服器直接根據token取出儲存的使用者資訊 即可對token的可用性進行校驗 使得單點登入更為簡單

2、JWT校驗的過程

1、瀏覽器傳送使用者名稱和密碼 發起登入請求
2、服務端驗證身份 根據演算法將使用者識別符號打包生成token字串 並且返回給瀏覽器
3、當瀏覽器需要發起請求時 將token一起傳送給伺服器
4、伺服器發現數據中攜帶有token 隨即進行解密和鑑權
5、校驗成功 伺服器返回請求的資料

二、使用

1、首先是導包

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

<!-- Spring Security和JWT整合 -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-jwt</artifactId>
	<version>1.0.10.RELEASE</version>
</dependency>

<!-- JWT -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>

<!-- 字串轉換需要用到此包 -->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-lang3</artifactId>
	<version>3.4</version>
</dependency>

2、實體類

兩個實體類 一個是使用者 另一個是許可權

public class User {
  private Integer id;
  private String username;
  private String password;
  
	省略gettersetter之類的程式碼...
}
public class Role {
  private Integer id;
  private String username;
  private String name;
  
	省略gettersetter之類的程式碼...
}

3、然後需要一個Utils工具類

該類用於進行Token的加密和解密 可在此類中單元測試

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTokenUtil {
  // Token請求頭
  public static final String TOKEN_HEADER = "Authorization";
  // Token字首
  public static final String TOKEN_PREFIX = "Bearer ";

  // 簽名主題
  public static final String SUBJECT = "piconjo";
  // 過期時間
  public static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7;
  // 應用金鑰
  public static final String APPSECRET_KEY = "piconjo_secret";
  // 角色許可權宣告
  private static final String ROLE_CLAIMS = "role";
  
  /**
   * 生成Token
   */
  public static String createToken(String username,String role) {
    Map<String,Object> map = new HashMap<>();
    map.put(ROLE_CLAIMS,role);

    String token = Jwts
        .builder()
        .setSubject(username)
        .setClaims(map)
        .claim("username",username)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
        .signWith(SignatureAlgorithm.HS256,APPSECRET_KEY).compact();
    return token;
  }

  /**
   * 校驗Token
   */
  public static Claims checkJWT(String token) {
    try {
      final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
      return claims;
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * 從Token中獲取username
   */
  public static String getUsername(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.get("username").toString();
  }

  /**
   * 從Token中獲取使用者角色
   */
  public static String getUserRole(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.get("role").toString();
  }

  /**
   * 校驗Token是否過期
   */
  public static boolean isExpiration(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.getExpiration().before(new Date());
  }
}

4、配置UserDetailsService的實現類 用於載入使用者資訊

import xxx.xxx.xxx.bean.Role; // 自己的包
import xxx.xxx.xxx.bean.User; // 自己的包
import xxx.xxx.xxx.mapper.UserMapper; // 自己的包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

  @Autowired
  private UserMapper userMapper;

  @Override
  public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    if (s == null || "".equals(s))
    {
      throw new RuntimeException("使用者不能為空");
    }
    // 呼叫方法查詢使用者
    User user = userMapper.findUserByUsername(s);
    if (user == null)
    {
      throw new RuntimeException("使用者不存在");
    }
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    for (Role role:userMapper.findRoleByUsername(s))
    {
      authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName()));
    }
    return new org.springframework.security.core.userdetails.User(user.getUsername(),"{noop}"+user.getPassword(),authorities);
  }
}

5、然後 配置兩個攔截器

其中 一個用於登入 另一個用於鑑權

JWTAuthenticationFilter登入攔截器:

該攔截器用於獲取使用者登入的資訊
至於具體的驗證 只需建立一個token並呼叫authenticationManager的authenticate()方法
讓Spring security驗證即可 驗證的事交給框架

import com.alibaba.fastjson.JSON;
import xxx.xxx.xxx.utils.JwtTokenUtil; // 自己的包
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;

/**
 * 驗證使用者名稱密碼正確後 生成一個token並將token返回給客戶端
 */
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

  private AuthenticationManager authenticationManager;

  public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
  {
    this.authenticationManager = authenticationManager;
  }

  /**
   * 驗證操作 接收並解析使用者憑證
   */
  @Override
  public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {
    // 從輸入流中獲取到登入的資訊
    // 建立一個token並呼叫authenticationManager.authenticate() 讓Spring security進行驗證
    return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getParameter("username"),request.getParameter("password")));
  }

  /**
   * 驗證【成功】後呼叫的方法
   * 若驗證成功 生成token並返回
   */
  @Override
  protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException {
    User user= (User) authResult.getPrincipal();

    // 從User中獲取許可權資訊
    Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
    // 建立Token
    String token = JwtTokenUtil.createToken(user.getUsername(),authorities.toString());

    // 設定編碼 防止亂碼問題
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");
    // 在請求頭裡返回建立成功的token
    // 設定請求頭為帶有"Bearer "字首的token字串
    response.setHeader("token",JwtTokenUtil.TOKEN_PREFIX + token);

    // 處理編碼方式 防止中文亂碼
    response.setContentType("text/json;charset=utf-8");
    // 將反饋塞到HttpServletResponse中返回給前臺
    response.getWriter().write(JSON.toJSONString("登入成功"));
  }

  /**
   * 驗證【失敗】呼叫的方法
   */
  @Override
  protected void unsuccessfulAuthentication(HttpServletRequest request,AuthenticationException failed) throws IOException,ServletException {
    String returnData="";
    // 賬號過期
    if (failed instanceof AccountExpiredException) {
      returnData="賬號過期";
    }
    // 密碼錯誤
    else if (failed instanceof BadCredentialsException) {
      returnData="密碼錯誤";
    }
    // 密碼過期
    else if (failed instanceof CredentialsExpiredException) {
      returnData="密碼過期";
    }
    // 賬號不可用
    else if (failed instanceof DisabledException) {
      returnData="賬號不可用";
    }
    //賬號鎖定
    else if (failed instanceof LockedException) {
      returnData="賬號鎖定";
    }
    // 使用者不存在
    else if (failed instanceof InternalAuthenticationServiceException) {
      returnData="使用者不存在";
    }
    // 其他錯誤
    else{
      returnData="未知異常";
    }

    // 處理編碼方式 防止中文亂碼
    response.setContentType("text/json;charset=utf-8");
    // 將反饋塞到HttpServletResponse中返回給前臺
    response.getWriter().write(JSON.toJSONString(returnData));
  }
}

JWTAuthorizationFilter許可權校驗攔截器:

當訪問需要許可權校驗的URL(當然 該URL也是需要經過配置的) 則會來到此攔截器 在該攔截器中對傳來的Token進行校驗
只需告訴Spring security該使用者是否已登入 並且是什麼角色 擁有什麼許可權即可

import xxx.xxx.xxx.utils.JwtTokenUtil; // 自己的包
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

/**
 * 登入成功後 走此類進行鑑權操作
 */
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

  public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
    super(authenticationManager);
  }

  /**
   * 在過濾之前和之後執行的事件
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request,FilterChain chain) throws IOException,ServletException {
    String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);

    // 若請求頭中沒有Authorization資訊 或是Authorization不以Bearer開頭 則直接放行
    if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX))
    {
      chain.doFilter(request,response);
      return;
    }

    // 若請求頭中有token 則呼叫下面的方法進行解析 並設定認證資訊
    SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
    super.doFilterInternal(request,response,chain);
  }

  /**
   * 從token中獲取使用者資訊並新建一個token
   *
   * @param tokenHeader 字串形式的Token請求頭
   * @return 帶使用者名稱和密碼以及許可權的Authentication
   */
  private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
    // 去掉字首 獲取Token字串
    String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX,"");
    // 從Token中解密獲取使用者名稱
    String username = JwtTokenUtil.getUsername(token);
    // 從Token中解密獲取使用者角色
    String role = JwtTokenUtil.getUserRole(token);
    // 將[ROLE_XXX,ROLE_YYY]格式的角色字串轉換為陣列
    String[] roles = StringUtils.strip(role,"[]").split(",");
    Collection<SimpleGrantedAuthority> authorities=new ArrayList<>();
    for (String s:roles)
    {
      authorities.add(new SimpleGrantedAuthority(s));
    }
    if (username != null)
    {
      return new UsernamePasswordAuthenticationToken(username,null,authorities);
    }
    return null;
  }
}

6、再配置一個自定義類 用於進行匿名使用者訪問資源時無許可權的處理

該類需實現AuthenticationEntryPoint

import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request,AuthenticationException authException) throws IOException,ServletException {
    response.setCharacterEncoding("utf-8");
    response.setContentType("text/javascript;charset=utf-8");
    response.getWriter().print(JSONObject.toJSONString("您未登入,沒有訪問許可權"));
  }
}

7、最後 將這些元件組裝到一起即可

建立一個自定義的配置類 繼承WebSecurityConfigurerAdapter
在該類上 需加@EnableWebSecurity註解 配置Web安全過濾器和啟用全域性認證機制

import xxx.xxx.xxx.JWTAuthenticationEntryPoint; // 自己的包
import xxx.xxx.xxx.xxx.JWTAuthenticationFilter; // 自己的包
import xxx.xxx.xxx.xxx.JWTAuthorizationFilter; // 自己的包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  @Qualifier("userDetailsServiceImpl")
  private UserDetailsService userDetailsService;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
  }

  /**
   * 安全配置
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // 跨域共享
    http.cors()
        .and()
        // 跨域偽造請求限制無效
        .csrf().disable()
        .authorizeRequests()
        // 訪問/data需要ADMIN角色
        .antMatchers("/data").hasRole("ADMIN")
        // 其餘資源任何人都可訪問
        .anyRequest().permitAll()
        .and()
        // 新增JWT登入攔截器
        .addFilter(new JWTAuthenticationFilter(authenticationManager()))
        // 新增JWT鑑權攔截器
        .addFilter(new JWTAuthorizationFilter(authenticationManager()))
        .sessionManagement()
        // 設定Session的建立策略為:Spring Security永不建立HttpSession 不使用HttpSession來獲取SecurityContext
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        // 異常處理
        .exceptionHandling()
        // 匿名使用者訪問無許可權資源時的異常
        .authenticationEntryPoint(new JWTAuthenticationEntryPoint());
  }

  /**
   * 跨域配置
   * @return 基於URL的跨域配置資訊
   */
  @Bean
  CorsConfigurationSource corsConfigurationSource() {
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    // 註冊跨域配置
    source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues());
    return source;
  }
}

定義一個用於測試的對外對映介面:

@RestController
public class UserController {

  @GetMapping("/data")
  private ResponseUtil data()
  {
    return "This is data.";
  }
}

預設登入路徑是/login 用POST請求傳送

若要修改預設的登入路徑 只需要在自己定義的登入過濾器JWTAuthenticationFilter的構造方法裡進行配置即可
比如 若想修改為/api/login:

public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
{
   this.authenticationManager = authenticationManager;
   // 設定登入URL
   super.setFilterProcessesUrl("/api/login");
}

登入時 引數的屬性名分別是username和password 不能改動:

SpringBoot整合Spring Security用JWT令牌實現登入和鑑權的方法

登入成功後會返回一個Token:

SpringBoot整合Spring Security用JWT令牌實現登入和鑑權的方法

在請求需要許可權的介面路徑時 若不帶上Token 則會提示沒有訪問許可權

SpringBoot整合Spring Security用JWT令牌實現登入和鑑權的方法

帶上Token後再次請求 即可正常訪問:

注:Token的前面要帶有Bearer 的字首

SpringBoot整合Spring Security用JWT令牌實現登入和鑑權的方法

這樣 一個基本的實現就差不多完成了

為簡單演示 在該案例中就不對密碼進行加密了 實際開發是需要對明文密碼加密後儲存的 推薦用BCrypt進行加密和解密
為節省篇幅 用於註冊的介面也不寫了 實際上在註冊介面傳入的密碼也需要用BCrypt加密後再存入資料庫中
還可以用Redis進行Token的儲存 這些都是後話了

到此這篇關於SpringBoot整合Spring Security用JWT令牌實現登入和鑑權的方法的文章就介紹到這了,更多相關SpringBoot JWT令牌登入和鑑權內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!