Spring security 04-整合 jwt

Spring security 系列博客目錄

對應源代碼

預備知識

閱讀本文以前,首先要了解 Spring security 的相關內容,最起碼須要將我以前的系列文章看完。其次須要學習 jwt 的相關知識。關於 jwt(全稱JSON Web Token) ,推薦參考阮一峯大神的 JSON Web Token 入門教程,也可翻看其餘相關資料。html

需求分析

業務需求:後臺的一些接口須要用戶登陸(好比獲取用戶列表:user/list)時,纔有權限訪問。git

業務實現:客戶端訪問 user/list 接口時,須要在請求頭攜帶 token,而後後臺經過判斷 token 的有效性去選擇放行仍是攔截請求。github

業務難點:token 的有效性如何判斷?web

難點攻破:token的有效性藉助 jwt,因此須要充分理解 jwt 的相關知識。spring

業務流程:當訪問 user/list 接口時,後臺經過一個過濾器攔截,從請求頭中獲取token信息,而後經過 jwt工具類校驗token的有效性。json

建立工程

我本身是將上一篇文章中的工程複製了一份,而後作了些改動,此次改動的東西比較多(增長和刪除了一些類,增長了一些配置),固然我會盡可能講清楚作了哪些改動,因此你們自行選擇新建工程仍是複製一份。api

引入 pom 依賴

較之上一篇的內容,本次只新增了 jwt 的依賴包springboot

<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(Json Web Token)-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

修改 springboot 的 yml 配置文件

這裏能夠簡單看一下,根據 jwt 相關知識以及看後面的代碼會明白他們的意義。app

jwt:
  tokenHeader: Authorization # JWT存儲的請求頭
  secret: mySecret # JWT加解密使用的密鑰
  expiration: 604800 # JWT的超期限時間(60*60*24)
  tokenHead: Bearer  # JWT負載中拿到開頭

準備幾個公共類

只是圖個本身方便,從別處 copy 的工具類,須要你們簡單看下。三個類合在一塊兒的做用就是一個 json 格式的響應數據的工具。代碼放在 common 包下面。ide

  • CommonResult 響應工具類, 返回 json 數據給客戶端。例如:{code:200, message:"登陸成功",data:null}
  • IErrorCode 接口,定義了兩個方法
  • ResultCode 枚舉類 實現了 IErrorCode 接口,定義響應返回結果

封裝一個 jwt 的工具類

從別處拷貝了一個 JwtTokenUtil 類,裏面封裝了一些方法,諸如:生成token,判斷token的有效性等。不用糾結方法是如何實現的,只管拿來用(除非出現問題),方法見名知意。

定義登陸接口

在 UserController 類裏面定義一個登陸方法,以下所示。

  • 須要調用UserDetailService 的loadUserByUsername方法,若是看過我以前的文章,這裏應該清楚這個方法主要作用戶校驗的。

  • 調用 security 的 api, SecurityContextHolder.getContext().setAuthentication(authentication),能夠理解爲security認證用戶登陸成功

  • 生成 token 信息返回給客戶端,這個 token 是供客戶端調用其餘接口(須要用戶登陸才能調用的接口)時使用的。

    /**
    *  用戶登陸接口
    * [@param](https://my.oschina.net/u/2303379) username 用戶名
    * [@param](https://my.oschina.net/u/2303379) password 密碼
    * [@return](https://my.oschina.net/u/556800)
    */
    @PostMapping("/login")
    public Object login(@RequestParam("username") String username, @RequestParam("password") String password) {
    
      try {
    
          // 校驗用戶信息 
          UserDetails userDetails = userDetailsService.loadUserByUsername(username);
          // 保存用戶登陸態
          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
          SecurityContextHolder.getContext().setAuthentication(authentication);// 進行到這一步,security 纔會認爲你登錄成功
          // 根據用戶信息生成 token 
          String token = jwtTokenUtil.generateToken(userDetails);
          return CommonResult.success(token);// 到這裏, 從咱們的業務角度來講登錄成功了
      } catch (AuthenticationException e) {
          System.out.println("登陸異常: " + e.getMessage());
          return CommonResult.failed("登陸失敗");
      }

    }

定義一個過濾器

過濾器的功能就是用來攔截請求的 url ,而後校驗 token 的有效性。 SecurityContextHolder.getContext().setAuthentication(authentication); 這段代碼着重說一下,咱們能夠簡單理解爲 SecurityContextHolder.getContext() 爲 Security 提供的一個保存已登陸用戶信息的一個容器,經過調用它的 setAuthentication 以及getAuthentication方法,能夠將用戶信息存入容器和從容器中取出。

/**
 * JWT登陸受權過濾器
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        // 1. 從請求頭中取出 token ,key隨便定義,我這裏定義在了配置文件中
        String authHeader = request.getHeader(this.tokenHeader);
        
        // 2. 判斷是否爲空,以及是否以 "Bearer " 開頭       ps :固定格式 
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());// 必須以 "Bearer " 開頭
            // 3. 從 token 裏獲取用戶名
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            // 4. 只要 token 沒過時,就讓用戶保持登陸狀態 jwt 令牌只是輔助登陸, 真正是否登陸要看 authentication 是否有效
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                
                // 下面這段代碼就是爲了讓 security 保持用戶登陸狀態, 在UserController 裏你也能夠看到
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    // 核心代碼
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }

 }

將 JwtFilter 添加到 Spring Security 中

在 WebSecurityConfig 配置類的configure方法中的最後一行加上以下代碼。該方法中的其餘代碼的含義,不懂得能夠參考前幾篇博客。

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.csrf().disable();// 必須有, 否則會 403 forbidden

    /*http.formLogin()
            .loginPage("/loginPage.html")// 自定義登陸頁
            .loginProcessingUrl("/form/login")// 自定義登陸 action, 名字隨便起
            .successHandler(successHandler)// 自定義登陸成功處理類
            .failureHandler(failureHandler);// 自定義登陸失敗處理類*/

    // 訪問 "/form/login", "/loginPage.html"   放行
    http.authorizeRequests().antMatchers("/user/userInfo", "/user/login", "/form/login", "/loginPage.html").permitAll()
            .antMatchers("/hello").hasRole("superadmin") // 只有superadmin 角色的用戶才能訪問
            .anyRequest().authenticated();

    /*http.exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler)// 用戶沒有訪問權限處理器
            .authenticationEntryPoint(entryPoint);// 用戶沒有登陸處理器*/

    // 這裏是新增的代碼
    http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}

總結

至此,Sprng Securiy 系列博客更新完畢。這裏可能須要聲明一下, 個人一系列文章可能沒講 Spring Security 的工做流程,或者底層原理,全程在將如何使用 Spring Security。好比下面這段憑空而來的代碼,只是講了這段代碼的邏輯含義,沒有講清楚爲何這樣用。這些是我之後要努力完成的方向。只是但願幫助到須要的人,也歡迎你們給出意見和批評。

UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
    
    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    
    SecurityContextHolder.getContext().setAuthentication(authentication);
}
相關文章
相關標籤/搜索