Spring Security 分佈式認證

相關介紹

1)JWT
JWT:全稱 JSON Web Token,是一個分佈式身份校驗方案,可生產 token,也可解析 token
JWT生成的 token 由三部分組成:

  • 頭部:主要設置一些規範信息,簽名部分的編碼格式就在頭部聲明
  • 載荷:token 中存放有效信息的部分。比如用戶名、角色、過期時間等,切記不要放密碼,會泄露
  • 簽名:將頭部與載荷分別採用base64編碼後,用“.”相連,再加入鹽,最後使用頭部聲明的編碼類型進行編碼,就得到了簽名。

2)RSA 非對稱加密
基本原理:同時生成兩把密鑰(公鑰和私鑰),私鑰隱祕保存,公鑰可發放給信任的客戶端

  • 私鑰加密:持有私鑰或公鑰可解密
  • 公鑰加密:持有私鑰纔可解密

優點:安全,難以破解
缺點:算法耗時,但可以接受

3)項目說明
在這裏插入圖片描述

以下只貼關鍵代碼,需要完整代碼見本文末尾 github 鏈接

公共模塊

1)首先創建一個父工程,把其中 src 目錄刪除,添加 pom 依賴,主要添加 springboot 依賴


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

2)在該工程下創建子模塊----公共模塊
該模塊放了一些 JWT 和 RSA 的工具類,因此需要加入 jwt 和 json 相關的依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springsecurity_jwt_parent</artifactId>
        <groupId>com.xiao</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>common_module</artifactId>


    <dependencies>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.1</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.1</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.1</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

具體工具類見源碼>>源碼

認證模塊

1)認證模塊,主要對用戶信息進行校驗並生成 token。也算是一個完整的項目,需要實體類(用戶信息、角色信息)、service 、mapper等。
這些類的代碼可以參考《Spring Security 簡單認證與授權

2)配置文件

rsa:
  key:
    private-key-path: G:\temp\authRsa\rsa_id_key
    public-key-path: G:\temp\authRsa\rsa_id_key.pub

這個密鑰文件可以通過工具類的 密鑰生成方法生成,代碼在公共模塊的測試類中

3)JWT 的認證和驗證 過濾器

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
	//過濾器不放到容器中,無法直接注入,需通過構造器傳入
    private AuthenticationManager authenticationManager;
    private RsaKeyProperties rsaKeyProperties;

    public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties){
        this.authenticationManager = authenticationManager;
        this.rsaKeyProperties = rsaKeyProperties;
    }

	//認證邏輯
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {

        try {
        	//從請求中獲取用戶信息
            SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
            UsernamePasswordAuthenticationToken authRequest =
                    new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
            return authenticationManager.authenticate(authRequest);
        } catch (IOException e) {
        	//認證失敗,返回失敗信息
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            try {
                PrintWriter writer = response.getWriter();
                Map resultMap = new HashMap<>();
                resultMap.put("code",HttpServletResponse.SC_UNAUTHORIZED);
                resultMap.put("msg", "用戶名或密碼錯誤!");
                writer.write(new ObjectMapper().writeValueAsString(resultMap));
                writer.flush();
                writer.close();
            }catch(Exception ex) {
                ex.printStackTrace();
            }
            throw new RuntimeException(e);
        }

    }

	// 認證成功後,生成 token,返回 token
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SysUser sysUser = new SysUser();
        sysUser.setUsername(authResult.getName());  // 密碼是敏感信息,不放到頭部
        sysUser.setRoles((List<SysRole>) authResult.getAuthorities());
        String token = JwtUtils.generateTokenExpireInMinutes(sysUser, rsaKeyProperties.getPrivateKey(), 24 * 60);
        response.addHeader("Authorization", "Bearer " + token);
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        try {
            PrintWriter writer = response.getWriter();
            Map resultMap = new HashMap<>();
            resultMap.put("code",HttpServletResponse.SC_OK);
            resultMap.put("msg", "認證通過!");
            writer.write(new ObjectMapper().writeValueAsString(resultMap));
            writer.flush();
            writer.close();
        }catch(Exception ex) {
            ex.printStackTrace();
        }
    }


}
//驗證token是否正確
public class JwtVerifyFilter extends BasicAuthenticationFilter {
    private RsaKeyProperties rsaKeyProperties;

    public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
        super(authenticationManager);
        this.rsaKeyProperties = rsaKeyProperties;
    }

    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if(header == null || !header.startsWith("Bearer")){
            chain.doFilter(request, response);
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter writer = response.getWriter();
            Map resultMap = new HashMap<>();
            resultMap.put("code",HttpServletResponse.SC_FORBIDDEN);
            resultMap.put("msg", "請登錄!");
            writer.write(new ObjectMapper().writeValueAsString(resultMap));
            writer.flush();
            writer.close();
        }else {
            //攜帶了正確格式的token
            String token = header.replace("Bearer", "");
            //驗證token是否正確
            Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class);
            //獲取到當前登錄用戶的信息
            SysUser userInfo = payload.getUserInfo();
            if(userInfo != null) {
                UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(userInfo.getUsername(), "", userInfo.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authResult);
                chain.doFilter(request, response);
            }
        }

    }

}

4)配置類主要需要配置 RSA 和 springsecurity
從配置文件綁定屬性到實體類中

@ConfigurationProperties(prefix = "rsa.key")
public class RsaPath {
    private String publicKeyPath;
    private String privateKeyPath;

    public String getPublicKeyPath() {
        return publicKeyPath;
    }

    public void setPublicKeyPath(String publicKeyPath) {
        this.publicKeyPath = publicKeyPath;
    }

    public String getPrivateKeyPath() {
        return privateKeyPath;
    }

    public void setPrivateKeyPath(String privateKeyPath) {
        this.privateKeyPath = privateKeyPath;
    }

}
@Component
public class RsaKeyProperties {

    @Autowired
    RsaPath rsaPath;

    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct     //該註釋確保publicKeyPath和privateKeyPath都有值後才執行該方法
    public void getRsaKey() throws Exception {
        System.out.println(rsaPath.getPublicKeyPath());
        publicKey = RsaUtils.getPublicKey(rsaPath.getPublicKeyPath());
        privateKey = RsaUtils.getPrivateKey(rsaPath.getPrivateKeyPath());
    }

    public PublicKey getPublicKey() {
        return publicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
        this.publicKey = publicKey;
    }

    public PrivateKey getPrivateKey() {
        return privateKey;
    }

    public void setPrivateKey(PrivateKey privateKey) {
        this.privateKey = privateKey;
    }
}
@Configuration
@EnableWebSecurity
public class SpringsecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SysUserService userService;
    @Autowired
    private RsaKeyProperties rsaKeyProperties;

    //配置加密
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); //spring security 內置加密算法
    }

    //認證用戶的來源
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    //Spring Security配置
    public void configure(HttpSecurity hs) throws Exception {
        hs.csrf()
            .disable()
            .authorizeRequests()
            .antMatchers("/**").hasAnyRole("NORMAL")
            .anyRequest().authenticated()

            .and()  //綁定過濾器
            .addFilter(new JwtLoginFilter(super.authenticationManager(), rsaKeyProperties))
            .addFilter(new JwtVerifyFilter(super.authenticationManager(), rsaKeyProperties))
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //分佈式不需要session
    }
}

5)測試
使用 postman 進行測試(用到的數據庫數據可參考上一篇《Spring Security 簡單認證與授權》)
登錄認證 > 攜帶返回的 token 訪問資源
在這裏插入圖片描述

在這裏插入圖片描述

資源模塊

該模塊大部分內容與認證模塊相同。該模塊不能放置私鑰,所以需要把跟私鑰相關的信息刪除即可。跟認證的邏輯有關的內容也要刪除,因爲該模塊不需要用戶登錄,只校驗其攜帶的 token。

該模塊主要是需要校驗請求攜帶的 token 是否有效,有效則允許訪問資源,否則拒絕訪問。

具體代碼可參考源碼

測試
無需進行登錄,把剛剛在認證模塊返回的、還未失效的 token 拿過來去請求訪問資源即可。

本項目 github 地址:https://github.com/godXiaogf/springsecurity_jwt_parent_idea

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章