從零玩轉SpringSecurity+JWT整合前后端分離


從零玩轉SpringSecurity+JWT整合前后端分離

2021年4月9日 · 預計閱讀時間: 50 分鍾

一、什么是Jwt?

Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於 JSON 的開放標准

((RFC 7519).該 token 被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景

JWT 的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服

務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該 token 也可直接被用於

認證,也可被加密。

官網:https://jwt.io/introduction/

簡單的說:jwt就是一個json字符串 它可以存儲任何信息。

內置了校驗-我們只需要請求時給到它生產出來的token令牌即可解析到我們存儲進去的信息。

1.創建jwtDemo Maven工程

 <!-- 添加 jwt 的依賴 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.11.0</version>
        </dependency>

2.創建jwtTest.class

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

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

/**
 * ClassName: JwtTest
 * 個人博客: https://yangbuyi.top
 * @author yangbuyiya
 * @Date: 2021-03-25 10:05
 * @Description: $
 **/
public class JwtTest {

    /**
     * 加密jwt
     *
     * @param username
     * @return
     */
    public static String createJwt(String username) {
        // 頒發時間
        Date createTime = new Date();
        // 過期時間
        Calendar now = Calendar.getInstance();
        // 設置未來時間 秒
        now.set(Calendar.SECOND, 7200);
        Date expireTime = now.getTime();
        // header
        Map<String, Object> header = new HashMap<>(4);
        header.put("alg", "HS256");
        header.put("type", "JWT");
        // 載體
        return JWT.create()
                // 設置頭部信息
                .withHeader(header)
                // 設置創建時間
                .withIssuedAt(createTime)
                // 設置過期時間
                .withExpiresAt(expireTime)
                // 設置主體
                .withSubject("這是一個JWT")
                // 設置載荷--也就是用戶信息
                .withClaim("username", username)
                .withClaim("pwd", "123456")
                // 設置簽名密鑰
                .sign(Algorithm.HMAC256("yby-jwt"));
    }

    /**
     * 解密jwt
     *
     * @param jwt
     * @return
     */
    public static boolean decryptJwt(String jwt) {
        // 帶入密鑰解密
        JWTVerifier require = JWT.require(Algorithm.HMAC256("yby-jwt")).build();
        try {
            DecodedJWT verify = require.verify(jwt);
            // 根據設置的key獲取對應的value
            Claim username = verify.getClaim("username");
            System.out.println(username.asString());
            System.out.println(verify.getSignature());
            System.out.println(verify.getSubject());
            return true;
        } catch (JWTVerificationException e) {
            e.printStackTrace();
            return false;
        }
    }

    public static void main(String[] args) {
        // 創建令牌
        System.out.println(createJwt("楊不易"));
       
 // 解析令牌       System.out.println(decryptJwt("eyJ0eXAiOiJKV1QiLCJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiLov5nmmK_kuIDkuKpKV1QiLCJleHAiOjE2MTcwODAyMjAsInB3ZCI6IjEyMzQ1NiIsImlhdCI6MTYxNzA3MzAzMSwidXNlcm5hbWUiOiLmnajkuI3mmJMifQ.GJOeFSVsAwFPcgUlalmxVXt0QQ-Be5bhGUtL1ep04vM"));
    }
}

3.測試jwt

創建令牌

解析令牌信息

4.JWT的總結

JWT就是一個加密的帶用戶信息的字符串,沒學習JWT之前,我們在項目中都是返回一個基本的

字符串,然后請求時帶上這個字符串,再從session或者redis中(共享session)獲取當前用戶,

學過JWT以后我們可以把用戶信息直接放在字符串返回給前段,然后用戶請求時帶過來,我們是在

服務器進行解析拿到當前用戶,這就是兩種登錄方式,這兩種方式有各自的優缺點,我們在后面

Oauth2.0+jwt中詳細學習

二、什么是SpringSecurity?

Spring Security 是一個能夠為基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案 的安全框架。它提供了一組可以在 Spring 應用上下文中配置的 Bean,充分利用了 Spring IoC, DI(控制反轉 Inversion of Control ,DI:Dependency Injection 依賴注入)和 AOP(面向切 面編程)功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量 重復代碼的工作。 以上解釋來源於百度百科。可以一句話來概括,SpringSecurity 是一個安全框架。

1.Spring Security 入門體驗

創建項目 springsecurity-hello

創建Controller請求訪問

啟動測試訪問

http://127.0.0.1:8080/hello

發現我們無法訪問 hello 這個請求,這是因為 spring Security 默認攔截了所有請求

我們在啟動日志當中復制密碼

用戶名默認是 user 哦

登錄成功之后訪問 controller

測試退出

頁面當中輸入: http://127.0.0.1:8080/logout

自定義密碼登錄(yml 配置文件方式)

spring:
  security:
   user:
   name: admin #默認使用的用戶名
   password: 123456 #默認使用的密碼

重啟使用 admin 和 123456 登錄即可

總結

從上面的體驗來說,是不是感覺很簡單,但是別急。后面的東西還是有點難度的,

如下:

如何讀取數據庫的用戶名和密碼

如何對密碼加密

如何使用數據的角色和權限

如何配置方法級別的權限訪問

如何自定義登陸頁面

如何集成 redis 把登陸信息放到 Redis

.............................

2.Spring Security 配置多用戶認證

概述

認證就是登陸,我們現在沒有連接數據庫,那么我們可以模擬下用戶名和密碼

/**
 * @Author 楊不易呀
 * web 安全的配置類
 * <p>
 * WebSecurityConfigurerAdapter   web安全配置的適配器
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 
{


    /**
     * 配置認證(用戶)管理 模擬內存用戶數據
	* 重點說明:
	* 在開發中,我們一般只針對權限,很少去使用角色
	* 后面的講解中我們以權限為主也就是 authorities 這里面的東西
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在內存中創建了兩個用戶
        // 注意點: 我們添加了安全配置類,那么我們在 yml 里面的用戶密碼配置就失效了
        auth.inMemoryAuthentication()
                .withUser("yby") // 用戶名
                .password("yby") // 密碼 
                .roles("ADMIN_yby") // 給了一個角色
                .authorities("sys:add", "sys:update", "sys:delete", "sys:select") 
                // 注意點:給yby用戶四個權限 如果權限和角色都給了 那么角色就失效了
                .and()
                .withUser("test")
                .password("test")
                .roles("TEST")
                .authorities("sys:select") // 加了一個權限
        ;
    }
}

1.啟動測試

使用yby/yby登錄訪問 可以發現 控制台報錯了

這個是因為 spring Sercurity 強制要使用密碼加密,當然我們也可以不加密,但是官方要求是不 管你是否加密,都必須配置一個類似 Shiro 的憑證匹配器

2.添加密碼加密器

    /**
     * 給容器中放一個加密器 springSecurity5.x之后推薦使用加密
     * 也可以不給加密
     * new NoOpPasswordEncoder()
     * 這個加密器對同一個值加密后 會得到不同的結果
     * 只要是用同一個加密器加密的 解密也是一樣的
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

3.修改用戶配置

 // 在內存中創建了兩個用戶
 // 注意點: 我們添加了安全配置類,那么我們在 yml 里面的用戶密碼配置就失效了
        auth.inMemoryAuthentication()
                .withUser("yby") // 用戶名
                .password(passwordEncoder().encode("yby") // 密碼 
                .roles("ADMIN_yby") // 給了一個角色
                .authorities("sys:add", "sys:update", "sys:delete", "sys:select") 
                // 注意點:給yby用戶四個權限 如果權限和角色都給了 那么角色就失效了
                .and()
                .withUser("test")
                .password(passwordEncoder().encode("test")
                .roles("TEST")
                .authorities("sys:select")
        ;

4.重啟測試

兩個用戶都可以登錄成功l了 恭喜恭喜!!!!

5.測試加密和解密 demo

public class TestPasswordEncoder {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode1 = passwordEncoder.encode("123");
System.out.println(encode1);
String encode2 = passwordEncoder.encode("123");
System.out.println(encode2);
String encode3 = passwordEncoder.encode("123");
System.out.println(encode3);
// 查看加密后是否匹配
System.out.println(passwordEncoder.matches("123", encode1));
System.out.println(passwordEncoder.matches("123", encode2));
System.out.println(passwordEncoder.matches("123", encode3));
}
}

查看控制台發現特點是:相同的字符串加密之后的結果都不一樣,但是比較的時候是一樣的,這個 算法比 shiro 的 MD5 好用,不用自己在數據庫去存鹽了

3.如何獲取當前登錄用戶的信息(兩種方式)

1.往HelloController添加請求

/**
* 獲取當前用戶信息,直接在參數中注入 Principal 對象
* 此對象是登錄后自動寫入 UsernamePasswordAuthenticationToken 類中
*
* @param principal
* @return
*/
@GetMapping("userInfo")
public Principal getUserInfo(Principal principal) {
return principal;
}
/**
* SecurityContextHolder.getContext()獲取安全上下文對象
* 就是那個保存在 ThreadLocal 里面的安全上下文對象
* 總是不為 null(如果不存在,則創建一個 authentication 屬性為 null 的 empty 安全上下文對象)
* 獲取當前認證了的 principal(當事人),或者 request token (令牌)
* 如果沒有認證,會是 null,該例子是認證之后的情況
*/
@GetMapping("userInfo2")
public Object getUserInfo2() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
ret

2.請求任意一個都可以獲取到登陸后的json信息

4.Spring Security 用戶,角色,權限攔截配置講解

1.角色和權限的配置,修改 WebSecurityConfig 類

 /**
     * 配置認證(用戶)管理 模擬內存用戶數據
     * 重點說明:
     * 在開發中,我們一般只針對權限,很少去使用角色
     * 后面的講解中我們以權限為主也就是 authorities 這里面的東西
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在內存中創建了兩個用戶
        auth.inMemoryAuthentication()
                .withUser("yby") // 用戶名
                .password(passwordEncoder().encode("yby")) // 密碼 需要加密
                .roles("ADMIN_SXT") // 給了一個角色
                .authorities("sys:add", "sys:update", "sys:delete", "sys:select") // 給yby用戶四個權限 如果權限和角色都給了 那么角色就失效了

                // 測試用戶 只有一個查看權限
                .and()
                .withUser("test")
                .password(passwordEncoder().encode("test"))
                .roles("TEST") // 失效 因為有 authorities這個了
                .authorities("sys:select")

                // admin用戶 角色權限區分
                .and()
                .withUser("admin")
                .password(passwordEncoder().encode("admin"))
                .roles("ADMIN") // 注意點:不能帶入前綴ROLE_ security里面默認會添加的 最終結果是  ROLE_ADMIN
        ;
    }




/**
* 配置 http 請求驗證等
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 注釋掉他自己的方法 走我們自己的
    // super.configure(http);
    // 給一個表單登陸 就是我們的登錄頁面,登錄成功或者失敗后走我們的 url
    http.formLogin()
        .successForwardUrl("/welcome") // 登錄成功走的url
        .failureForwardUrl("/fail") // 登錄失敗走的url
        .permitAll();
    // 匹配哪些 url,需要哪些權限才可以訪問 當然我們也可以使用鏈式編程的方式
    http.authorizeRequests()
        .antMatchers("/query").hasAuthority("sys:query") // 表示這個用戶有這個權限標識才能訪問
        .antMatchers("/save").hasAuthority("sys:save")
        .antMatchers("/del").hasAuthority("sys:del")
        .antMatchers("/update").hasAuthority("sys:update")
        .antMatchers("/admin/**").hasRole("ADMIN") // 表示這個用戶有這個角色才能訪問
    ; // 其他所有的請求都需要登錄才能進行
    // 所有的請求都需要認證才可以訪問
    http.authorizeRequests().anyRequest().authenticated();
}

2.創建AuthorityController 演示權限訪問

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 楊不易呀
 */
@RestController
public class AuthorityController {

    /**
     * 登錄成功的主頁返回值
     *
     * @return
     */
    @PostMapping("welcome")
    public String welcome() {
        return "歡迎來到主頁";
    }

    /**
     * 登錄失敗的返回值
     *
     * @return
     */
    @PostMapping("fail")
    public String fail() {
        return "登錄失敗了";
    }

    /**
     * 開啟方法權限的注解
     *
     * @return
     */
    @GetMapping("add")
    public String add() {
        return "歡迎來到主ADD";
    }

    @GetMapping("update")
    public String update() {
        return "歡迎來到UPDATE";
    }

    @GetMapping("delete")
    public String delete() {
        return "歡迎來到DELETE";
    }

    @GetMapping("select")
    public String select() {
        return "歡迎來到SELECT";
    }

    @GetMapping("role")
    public String role() {
        return "歡迎來到ROLE";
    }
    
    @GetMapping("admin/hello")
    public String admin() {
    return "我是只有 admin 角色才可以訪問的";
    }

}

3.創建訪問403權限不足頁面.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>沒有訪問權限哦</title>
</head>
<body>
<h1 style="color: red">您沒有訪問權限</h1>
</body>
</html>

會自動的跳轉到該目錄下.

訪問該用戶沒有的權限請求

5.Spring Security 返回 JSON(前后端分離)

在上面的例子中,我們返回的是 403 頁面,但是在開發中,如 RestAPI 風格的數據,是不能返回一 個頁面,而應該是給一個 json

1.添加處理器 RestAuthorizationAccessDeniedHandler

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

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

/**
 * @Author 楊不易呀
 * 自定義登錄成功的處理器
 * 返回json
 */
@Configuration
public class AuthenticateSuccess implements AuthenticationSuccessHandler {


    /**
     * 登陸成功后執行的處理器
     *
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登錄成功了");
        // 把json串寫出去
        response.setContentType("application/json;charset=utf-8");
        HashMap<String, Object> map = new HashMap<>(8);
        map.put("code", 200);
        map.put("msg", "登錄成功");
        // 把用戶信息返回給前端 讓前端可以保存起來
        map.put("data", authentication);
        ObjectMapper objectMapper = new ObjectMapper();
        String s = objectMapper.writeValueAsString(map);
        // 寫出去
        PrintWriter writer = response.getWriter();
        writer.write(s);
        // 刷新流 關閉流
        writer.flush();
        writer.close();
    }
}

2.修改WebSecurityConfig配置文件

/**
* 將自定義的拒絕訪問處理器注入進來
*/
@Autowired
private AccessDeniedHandler accessDeniedHandler;

/**
* 配置 http 請求驗證等
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定義403請求返回json
    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
    // 給一個表單登陸 就是我們的登錄頁面,登錄成功或者失敗后走我們的 url
    http.formLogin()
        .successForwardUrl("/welcome") // 登錄成功走的url
        .failureForwardUrl("/fail") // 登錄失敗走的url
        .permitAll();
    // 匹配哪些 url,需要哪些權限才可以訪問 當然我們也可以使用鏈式編程的方式
    http.authorizeRequests()
        .antMatchers("/query").hasAuthority("sys:query") // 表示這個用戶有這個權限標識才能訪問
        .antMatchers("/add").hasAuthority("sys:add")
        .antMatchers("/delete").hasAuthority("sys:delete")
        .antMatchers("/update").hasAuthority("sys:update")
        .antMatchers("/admin/**").hasRole("ADMIN") // 表示這個用戶有這個角色才能訪問
    ; // 其他所有的請求都需要登錄才能進行
    // 所有的請求都需要認證才可以訪問
    http.authorizeRequests().anyRequest().authenticated();
}

3.重新啟動訪問用戶沒有權限的url下·

4.登錄成功或者失敗都返回 JSON,我們需要自定義處理器

創建 AuthenticateSuccess

登錄成功返回json

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

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

/**
 * @Author 楊不易呀
 * 自定義登錄成功的處理器
 * 返回json
 */
@Configuration
public class AuthenticateSuccess implements AuthenticationSuccessHandler {


    /**
     * 登陸成功后執行的處理器
     *
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登錄成功了");
        // 把json串寫出去
        response.setContentType("application/json;charset=utf-8");
        HashMap<String, Object> map = new HashMap<>(8);
        map.put("code", 200);
        map.put("msg", "登錄成功");
        // 把用戶信息返回給前端 讓前端可以保存起來
        map.put("data", authentication);
        ObjectMapper objectMapper = new ObjectMapper();
        String s = objectMapper.writeValueAsString(map);
        // 寫出去
        PrintWriter writer = response.getWriter();
        writer.write(s);
        // 刷新流 關閉流
        writer.flush();
        writer.close();
    }
}

修改配置 http 請求驗證

注入登錄失敗和登錄成功返回json

    /**
     * 自定義登錄成功返回json
     */
	@Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

/**
* 配置 http 請求驗證等
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 給一個表單登陸 就是我們的登錄頁面,登錄成功或者失敗后走我們的 url
    //http.formLogin()
       // .successForwardUrl("/welcome") // 登錄成功走的url
        //.failureForwardUrl("/fail") // 登錄失敗走的url
       // .permitAll();
    // 這里使用了前后端分離的模式 實現我們的登錄成功和失敗返回json
    http.formLogin()
        .successHandler(authenticationSuccessHandler)
        .failureHandler(authenticationFailureHandler());
    // 匹配哪些 url,需要哪些權限才可以訪問 當然我們也可以使用鏈式編程的方式
    http.authorizeRequests()
        .antMatchers("/query").hasAuthority("sys:query") // 表示這個用戶有這個權限標識才能訪問
        .antMatchers("/save").hasAuthority("sys:save")
        .antMatchers("/del").hasAuthority("sys:del")
        .antMatchers("/update").hasAuthority("sys:update")
        .antMatchers("/admin/**").hasRole("ADMIN") // 表示這個用戶有這個角色才能訪問
    ; // 其他所有的請求都需要登錄才能進行
    // 所有的請求都需要認證才可以訪問
    http.authorizeRequests().anyRequest().authenticated();
}

/**
     * 登錄失敗的處理器
     *
     * @return
     */
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return (request, response, exception) -> {
            response.setContentType("application/json;charset=utf-8");
            System.out.println(exception);
            // 有很多登錄失敗的異常
            HashMap<String, Object> map = new HashMap<>(4);
            map.put("code", 401);
            // instanceof 判斷左右是否是右邊的 一個實例  這里的exception已經是一個具體的錯誤了
            if (exception instanceof LockedException) {
                map.put("msg", "賬戶被鎖定,登陸失敗!");
            } else if (exception instanceof BadCredentialsException) {
                map.put("msg", "賬戶或者密碼錯誤,登陸失敗!");
            } else if (exception instanceof DisabledException) {
                map.put("msg", "賬戶被禁用,登陸失敗!");
            } else if (exception instanceof AccountExpiredException) {
                map.put("msg", "賬戶已過期,登陸失敗!");
            } else if (exception instanceof CredentialsExpiredException) {
                map.put("msg", "密碼已過期,登陸失敗!");
            } else {
                map.put("msg", "登陸失敗!");
            }
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(map);
            PrintWriter writer = response.getWriter();
            writer.write(s);
            writer.flush();
            writer.close();
        };
    }

5.重新啟動工程進行登錄測試json返回

6.Spring Security 方法授權 權限訪問限制

我們使用方法級別的授權后,只需要在 controller 對應的方法上添加注解即可了,不需要再 webSecurityConfig 中配置匹配的 url 和權限了,這樣就爽多了

1.相關注解說明

@PreAuthorize 在方法調用前進行權限檢查

@PostAuthorize 在方法調用后進行權限檢查

@Secured 上面的三個注解如果要使用的話必須加上

@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)

如果只使用 PreAuthorize 就只用開啟 prePostEnabled = true

如果只使用@Secured 就只用開啟 securedEnabled = true 這種方式不推薦,有坑 坑在這里 @Secured,而@Secured 對應的角色必須要有 ROLE_前綴

2.在 WebSecurityConfig 類或者啟動類上添加注解

3.注釋掉 WebSecurityConfig 配置 url 和權限的代碼

4.修改 controller,給方法添加注解

不加注解的,都可以訪問,加了注解的,要有對應權限才可以訪問哦

    /**
     * 開啟方法權限的注解
     *
     * @return
     */
    @GetMapping("add")
    @PreAuthorize("hasAuthority('sys:add')")
    public String add() {
        return "歡迎來到主ADD";
    }

    @GetMapping("update")
    @PreAuthorize("hasAuthority('sys:update')")
    public String update() {
        return "歡迎來到UPDATE";
    }

    @GetMapping("delete")
    @PreAuthorize("hasAuthority('sys:delete')")
    public String delete() {
        return "歡迎來到DELETE";
    }

    @GetMapping("select")
    @PreAuthorize("hasAuthority('sys:select')")
    public String select() {
        return "歡迎來到SELECT";
    }

    @GetMapping("role")
    public String role() {
        return "歡迎來到ROLE";
    }

5.重新啟動即可。

整合Jwt+Security

二、創建jwt_secueiry工程

1 、創建工程完畢之后

2、修改啟動類 新增注解

3、修改配置文件 yml

server:
  port: 8080
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://yangbuyi.top/psringsecurity?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: xxxxxxxxxxxxxxxxxxxxxxxx
    password: xxxxxxxxxxxxxxxxxxxxxxxx
  redis:
    host: yangbuyi.top
    port: xxxxxxxxxxxxxxxxxxxxxx
    database: 0
    password: :xxxxxxxxxxxxxxxxxxxxxxxx
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

4、使用idea連接數據庫,進行代碼生成

可以看到除了persistent_logins 其它的就是RBAC表了, 如果不懂RBAC的小伙伴的話請自行百度學習即可 so easy to happy

右擊生成User表CRUD

5、創建完畢,開始配置SpringSecurity 的配置啦。
1. 創建WebSecurityConfig配置類
package top.yangbuyi.config;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.yangbuyi.constant.JwtConstant;

import java.io.PrintWriter;
import java.time.Duration;
import java.util.*;

/**
 * ClassName: WebSecurityConfig
 *
 * @author yangshuai
 * @Date: 2021-04-09 14:42
 * @Description: http請求配置 $
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 權限403返回json
     */
    @Autowired
    private AccessDecision accessDecision;

    /**
     * redis
     */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    /**
     * http請求
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        // 關閉csrf攻擊
        http.csrf().disable();
        // 登錄配置
        http.formLogin()
                .loginProcessingUrl("/doLogin") // 指定登錄地址
                .successHandler(authenticationSuccessHandler()) // 登錄成功執行的
                .failureHandler(authenticationFailureHandler()) // 登錄失敗執行的
                .permitAll();
        // 403 權限不足
        http.exceptionHandling().accessDeniedHandler(accessDecision);
        // 基於token方式 不會存儲session來進行登錄判斷了
        http.sessionManagement().disable();
        // 集成JWT 全部放行接口  使用jwt過濾器來進行 鑒權
        http.authorizeRequests().antMatchers("/**").permitAll();

    }


    /**
     * 登錄成功的 handle 
     *
     * @return
     */
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return (request,response,authentication) -> {
         System.out.println("登錄成功了");
        // 把json串寫出去
        response.setContentType("application/json;charset=utf-8");
        HashMap<String, Object> map = new HashMap<>(8);
        map.put("code", 200);
        map.put("msg", "登錄成功");
        // 把用戶信息返回給前端 讓前端可以保存起來
        map.put("data", authentication);
        ObjectMapper objectMapper = new ObjectMapper();
        String s = objectMapper.writeValueAsString(map);
        // 寫出去
        PrintWriter writer = response.getWriter();
        writer.write(s);
        // 刷新流 關閉流
        writer.flush();
        writer.close();
        };
    }

    /**
     * 登錄失敗的json
     *
     * @return
     */
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return (request, response, exception) -> {
            response.setContentType("application/json;charset=utf-8");
            // 寫出去
            HashMap<String, Object> map = new HashMap<>(4);
            map.put("code", 401);
            map.put("msg", "登陸失敗");
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(map);
            PrintWriter writer = response.getWriter();
            writer.write(s);
            writer.flush();
            writer.close();
        };
    }

}
新增jwt依賴
 <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.11.0</version>
        </dependency>

6.自定義配置用戶信息(數據庫當中獲取)

1.修改user實體類
@Data
@AllArgsConstructor
@NoArgsConstructor
public class sysUser implements Serializable, UserDetails {
    private Integer userid;

    private String username;

    private String userpwd;

    private String sex;

    private String address;

    private List<String> authors = Collections.emptyList();

    private static final long serialVersionUID = 1L;

    /**
     * 返回查詢出來的權限給到springSecurity
     *
     * @return the authorities, sorted by natural key (never <code>null</code>)
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorityArrayList = new ArrayList<>();
        if (this.authors.size() > 0) {
            authors.forEach(e -> authorityArrayList.add(new SimpleGrantedAuthority(e)));
        }
        return authorityArrayList;
    }

    /**
     * Returns the password used to authenticate the user.
     *
     * @return the password
     */
    @Override
    public String getPassword() {
        return this.userpwd;
    }

    /**
     * Indicates whether the user's account has expired. An expired account cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user's account is valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * Indicates whether the user is locked or unlocked. A locked user cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * Indicates whether the user's credentials (password) has expired. Expired
     * credentials prevent authentication.
     *
     * @return <code>true</code> if the user's credentials are valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * Indicates whether the user is enabled or disabled. A disabled user cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}
2.創建UserDetailsServiceImpl 到config配置下
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
import top.yangbuyi.domain.sysUser;
import top.yangbuyi.mapper.sysUserMapper;

import java.util.List;

/**
 * ClassName: UserDetailsServiceImpl
 *
 * @author yangshuai
 * @Date: 2021-04-09 14:55
 * @Description: 用戶信息配置 $
 **/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private sysUserMapper sysUserMapper;
    /**
     * 登錄時會來到這里進行 用戶校驗 相當於shiro當中的 認證用戶真實性
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根據用戶名稱查詢用戶信息
        sysUser sysUser = sysUserMapper.selectName(username);
        if (sysUser != null){
            List<String> a = sysUserMapper.selectAuthor(sysUser.getUserid());
            System.out.println("權限標識符:{}"+ a);
            sysUser.setAuthors(a);
        }
        return sysUser;
    }
}
3.修改mapper
    @Select(" select * from sys_user where username = #{username}  ")
    sysUser selectName(String username);

    @Select(" select distinct  sp.percode from sys_user_role sur left join sys_role_permission srp on sur.roleid = srp.roleid left join sys_permission sp on srp.perid = sp.perid where sur.userid = #{userid} ")
    List<String> selectAuthor(Integer userid);
4.修改WebSecurityConfig配置文件

4.啟動項目測試登錄

zhangsan/123456

相當於復習了上面的知識點只是將內存用戶改為自己數據庫當中的用戶

蕪湖!!!報錯了,咱們看到控制台打印 咦熟悉~~~~

加密器沒有注入欸,在SpringSeccurity當中是需要加密器來進行登錄的

來設置設置。。。

修改web配置文件
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

恭喜恭喜 登錄成功了

7.咦不對呀 不是要玩jwt嘛 ,不急 這就開始.....

思考:
1. jwt用來干啥啊????
哎,搞這么久還不知道? 用來搞權限啊 如果用戶沒有進行登錄 直接訪問我們的業務 那肯定不行的呀 那么帶着這些問題 我們沖沖沖!!!!!
2. 登錄時我們如何接入jwt呢?
在SpringSecurity登錄成功時會執行successHandel里面的自定義方法,我們在里面把用戶信息存儲到JWT然后呢
這時我們就需要使用到過濾器了,在springsecurity執行登錄前 我們自己來進行校驗登錄 即可

看我操作.......

1. 登錄成功后我們進行將登錄成功的用戶數據存儲到JWT 修改登錄成功的handel 用於jwt頒發token

 /**
     * 登錄成功的 handle
     * 1. 拿到用戶的信息
     * 2. 組裝JWT
     * 3. 寫出去
     *
     * @return
     */
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return (request,response,authentication) -> {

            // 1.拿去用戶數據
            UserDetails principal = (UserDetails) authentication.getPrincipal();
            String username = principal.getUsername();
            // 2.拿起權限
            Collection<? extends GrantedAuthority> authorities = principal.getAuthorities();
            List<String> authorizations = new ArrayList<>(authorities.size() * 2);
            authorities.forEach(e -> authorizations.add(e.getAuthority()));

            // 3.組裝jwt
            // 生成jwt
            // 頒發時間
            Date createTime = new Date();
            // 過期時間
            Calendar now = Calendar.getInstance();
            // 設置未來的時間
            now.set(Calendar.MINUTE, JwtConstant.EXPIRETIME);
            Date expireTime = now.getTime();
            // header
            HashMap<String, Object> header = new HashMap<>(4);
            header.put("alg", "HS256");
            header.put("typ", "JWT");
            // 設置載體
            String sign = JWT.create()
                    .withHeader(header)
                    .withIssuedAt(createTime)
                    .withExpiresAt(expireTime)
                    .withClaim("username", username)
                    .withClaim("authorizations", authorizations)
                    .sign(Algorithm.HMAC256(JwtConstant.SIGN));
            ObjectMapper objectMapper = new ObjectMapper();
            HashMap<String, Object> map = new HashMap<>(4);
            map.put("access_token", sign);
            map.put("expire_time", JwtConstant.EXPIRETIME);
            map.put("type", "bearer");
            // 保存redis  --- 客戶端和服務器來建立有狀態 
            redisTemplate.opsForValue().set(JwtConstant.JWT_KET + sign, sign, Duration.ofMinutes(JwtConstant.EXPIRETIME));
            String s = objectMapper.writeValueAsString(map);
            PrintWriter writer = response.getWriter();
            writer.write(s);
            writer.flush();
            writer.close();
        };
    }

2.創建JwtCheckToken類 用戶校驗用戶來的token是否合法並且是否由我頒發的

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import top.yangbuyi.constant.JwtConstant;

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.List;

/**
 * ClassName: JwtCheckFiter
 *
 * @author yangshuai
 * @Date: 2021-03-29 09:56
 * @Description: jwt鑒權 $
 **/
@Component
public class JwtCheckFilter extends OncePerRequestFilter {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 每一次請求都會進行執行改過濾器
     * 每次請求都會走這個方法
     * jwt 從header帶過來
     * 解析jwt
     * 設置到上下文當中去
     * jwt 性能沒有session好
     *
     * @param httpServletRequest
     * @param httpServletResponse
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        String requestURI = httpServletRequest.getRequestURI(); // 獲取請求 url 來判斷是否是登錄
        String method = httpServletRequest.getMethod(); // 請求類型
        if ("/doLogin".equals(requestURI) && "POST".equalsIgnoreCase(method)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        // 判斷是否帶入了token
        String header = httpServletRequest.getHeader(JwtConstant.AUTHORIZATION);
        if (StringUtils.hasText(header)) {
            // 解析真正的 token
            header = header.replaceAll(JwtConstant.JWTTYPE, "");
            // 判斷token是否存在
            Boolean aBoolean = stringRedisTemplate.hasKey(JwtConstant.JWT_KET + header);
            // 校驗是否過期 登出
            if (!aBoolean) {
                // 說明已經登出或者過期
                httpServletResponse.getWriter().write("token過期,請重新登錄!");
                return;
            }

            // 解析驗證
            JWTVerifier build = JWT.require(Algorithm.HMAC256(JwtConstant.SIGN)).build();
            DecodedJWT verify = null;
            try {
                verify = build.verify(header);
            } catch (JWTVerificationException e) {
                httpServletResponse.getWriter().write("token驗證失敗");
                return;
            }
            // 拿到解析后的jwt了 后端服務器沒保存用戶信息 給他設置進去
            Claim usernameClaim = verify.getClaim("username");
            String username = usernameClaim.asString();
            // 拿到權限
            Claim authsClaim = verify.getClaim("authorizations");
            List<String> authStrs = authsClaim.asList(String.class);
            // 轉變權限信息
            ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(authStrs.size() * 2);
            authStrs.forEach(auth -> simpleGrantedAuthorities.add(new SimpleGrantedAuthority(auth)));
            // 變成security認識的對象` 
            // 參數一: 用戶名稱
            // 參數二:密碼
            // 參數三:權限標識
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, simpleGrantedAuthorities);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 重新存儲到Security當中
            filterChain.doFilter(httpServletRequest, httpServletResponse); // 放行
            return;
        }
        httpServletResponse.getWriter().write("token驗證失敗.");
        return;
    }
}

3.修改web配置文件

將自定義的過濾器放在springSecurity執行登錄之前調用,用於校驗token 的合法權限

4.重新啟動測試token

登錄成功,返回token

根據token帶入 調用業務接口

好像忘記寫業務接口了 不過沒關系 補上不就行了

5.創建IndexController

/**
 * ClassName: IndexController
 *
 * @author yangshuai
 * @Date: 2021-04-09 16:11
 * @Description: 業務 $
 **/
@RestController
public class IndexController {

    @RequestMapping("query")
    @PreAuthorize("hasAuthority('sys:query')")
    public String query() {
        return "query";
    }

    @RequestMapping("add")
    @PreAuthorize("hasAuthority('sys:add')")
    public String add() {
        return "add";
    }

    @RequestMapping("delete")
    @PreAuthorize("hasAuthority('sys:delete')")
    public String delete() {
        return "delete";
    }

    @RequestMapping("update")
    @PreAuthorize("hasAuthority('sys:update')")
    public String update() {
        return "update";
    }

}

重新啟動測試token

可以帶入剛剛我們的登錄過的token 因為我們設置了過期時間 2小時嘛

欸 訪問成功 我們試一試 沒有權限的

8.JWT登出

思考:

1.咋記錄登錄的唯一性呀

思路:

在用戶登錄成功后進入到handel我們進行記錄當前jwt頒發的token到redis當中

然后就是,訪問登出接口 我們根據傳遞來的token前往redis當中查詢,判斷是否存在 存在刪除 即可。

這就引發出了我們自定義的 過濾器來校驗 上次登錄的token判斷redis當中是否存在 就判斷為 過期或者登出.

這段代碼功能我已經寫在Demo里面的IndexController了 小伙伴們課自己研究查看。

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @PostMapping("toLogout")
    public String toLogout() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String header = request.getHeader(JwtConstant.AUTHORIZATION);
        String jwtToken = header.replaceAll(JwtConstant.JWTTYPE, "");
        if (StringUtils.hasText(jwtToken)) {
            // 移除token
            stringRedisTemplate.delete(JwtConstant.JWT_KET + jwtToken);
        }
        return "退出成功!";
    }
到此 從零玩轉 jwt+SpirngSeccurity 就結束了哦!

我們下次再見....

個人博客網站: https://www.yangbuyi.top/

春天交流群 :598347590

本文Demo: https://gitee.com/yangbuyi/bky_yby

img

🍺你的壓力源於無法自律,只是假裝努力,現狀跟不上你內心的欲望,所以你焦急又恐慌---楊不易.|


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM