Springboot 整合spring Security和JWT 分布式版本

这一生的挚爱 提交于 2020-08-11 13:03:15

JWT介绍

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。


JWT使用场景

  • 授权
    常见的web项目中,当一个用户在A系统登录以后,后续的所有请求都将会包含JWT信息,允许用户访问该令牌(token)允许的路由,资源,服务等。现在常见的多系统之间的单点登录就是JWT的特性。

  • 信息交换 JWT可以被签名,可以使用密钥对确定服务器发送的消息的真实性等。


开发环境

  • openjdk11
  • springboot 2.3.1
  • spring security
  • Jwt

项目下载地址

https://gitee.com/randomObject/springboot-security-jwt-project


项目结构

  • springboot-security-jwt-project为父工程

父工程pom.xml内容如下

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.gitee.randomobject</groupId>
    <artifactId>springboot-security-jwt-project</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>springboot-common</module>
        <module>springboot-authServer</module>
        <module>springboot-resources</module>
    </modules>

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

    <properties>
        <java.version>11</java.version>
    </properties>

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

</project>

父工程pom.xml作用,控制springboot版本,子模块自动依赖父配置文件的依赖

  • springboot-common为公共模块

该模块pom.xml内容如下

<?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>springboot-security-jwt-project</artifactId>
        <groupId>com.gitee.randomobject</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springboot-common</artifactId>

    <dependencies>
        <!--jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <!--jwt-->
        <!--google joda time-->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.5</version>
        </dependency>
        <!--google joda time-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.68</version>
        </dependency>
    </dependencies>

</project>

springboot-commom模块处理一些公用的功能,比如jwt,fastjson,日志等依赖,工具类等。
项目结构

在此次示例项目中,common模块提供两个工具类负责生成RSA密钥的RSAUtils和负责加密解密token的JWTUtils


  • springboot-authServer为认证服务

springboot-authServer主要提供的服务为,认证当前请求者的权限和生成jwt给请求者。
项目结构:

其中认证和返回JWT信息给请求者的核心处理类在filter包下的JwtLoginFilter和VerifyJwtTokenFilter

熟悉Spring security的应该知道,springSecurity主要逻辑都在那十多个Filter中去实现的。可以说SpringSecurity是基于Filter的。

  • JwtLoginFilter 该类处理用户登录以后的认证和认证成功以后的操作。
    该类如下:
package com.gitee.randomobject.filter;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.gitee.randomobject.config.RSAProperties;
import com.gitee.randomobject.domain.SysRole;
import com.gitee.randomobject.domain.SysUser;
import com.gitee.randomobject.utils.JWTUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.io.PrintWriter;
import java.util.List;

/**
 * 分布式认证
 */
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {


    private AuthenticationManager authenticationManager;

    private RSAProperties rsaProperties;

    public JwtLoginFilter(AuthenticationManager authenticationManager, RSAProperties rsaProperties) {
        this.authenticationManager = authenticationManager;
        this.rsaProperties = rsaProperties;
    }

    /**
     * 认证参数
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        try {
            SysUser sysUser = JSONObject.parseObject(request.getInputStream(), SysUser.class, Feature.SafeMode);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    sysUser.getUsername(), sysUser.getPassword());
            return authenticationManager.authenticate(authRequest);
        } catch (Exception e) {
            try {
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter writer = response.getWriter();
                JSONObject result = new JSONObject();
                result.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                result.put("msg", "用户名或密码错误");
                writer.write(result.toJSONString());
                writer.flush();
                writer.close();
            } catch (Exception exception) {
                exception.printStackTrace();
            }
            throw new RuntimeException(e);
        }
    }

    /**
     *  返回登录成功以后的token
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    public void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        SysUser sysUser = new SysUser();
        sysUser.setUsername(authResult.getName());
        sysUser.setAuthorities((List<SysRole>) authResult.getAuthorities());
        //生成token
        String token = JWTUtils.generateToken(sysUser, rsaProperties.getPrivateKey(), 60);
        response.addHeader("Authorization", "Bearer "+token);
        try {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            PrintWriter writer = response.getWriter();
            JSONObject result = new JSONObject();
            result.put("code", HttpServletResponse.SC_OK);
            result.put("msg", "认证通过");
            writer.write(result.toJSONString());
            writer.flush();
            writer.close();
        } catch (Exception exception) {
            exception.printStackTrace();
        }

    }

}

UsernamePasswordAuthenticationFilter为SpringSecurity封装请求者传递的认证参数,比如用户名和口令等信息
attemptAuthentication()方法把前端传过来的用户名、口令等信息封装传给authenticationManager对象的authenticate()方法去认证。覆写这个方法,我们自己处理获得的用户名和密码,封装成UsernamePasswordAuthenticationToken对象,然后调用该方法正常的处理流程,唯一不同的是,我们没有从表单中获取传递过来的信息。
attemptAuthentication()方法原始的处理逻辑

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}

		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

successfulAuthentication()方法处理认证成功以后的逻辑,在这里,我们可以覆写,实现自己的逻辑,在该方法中我们可以返回生成的token给请求者。
successfulAuthentication()方法的原始逻辑

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}

		SecurityContextHolder.getContext().setAuthentication(authResult);

		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

  • VerifyJwtTokenFilter类处理用户请求头中是否包含token和验证token是否合法 该类内容如下
package com.gitee.randomobject.filter;

import com.alibaba.fastjson.JSONObject;
import com.gitee.randomobject.config.RSAProperties;
import com.gitee.randomobject.domain.SysUser;
import com.gitee.randomobject.domain.Payload;
import com.gitee.randomobject.utils.JWTUtils;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.io.PrintWriter;

/**
 * 验证请求头中是否携带token和token是否有效
 */
public class VerifyJwtTokenFilter extends BasicAuthenticationFilter {


    private RSAProperties rsaProperties;

    public VerifyJwtTokenFilter(AuthenticationManager authenticationManager, RSAProperties rsaProperties) {
        super(authenticationManager);
        this.rsaProperties = rsaProperties;
    }

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //从请求头中获取header
        String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {//未登录
            chain.doFilter(request, response);
            try {
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                PrintWriter writer = response.getWriter();
                JSONObject result = new JSONObject();
                result.put("code", HttpServletResponse.SC_FORBIDDEN);
                result.put("msg", "请登录");
                writer.write(result.toJSONString());
                writer.flush();
                writer.close();
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        } else {
            //验证token
            String token = header.replace("Bearer ", "");

            Payload<SysUser> userInfoFromToken = null;
            try {
                userInfoFromToken = JWTUtils.getUserInfoFromToken(token, rsaProperties.getPublicKey(), SysUser.class);
            } catch (SignatureException exception) {
                try {
                    response.setContentType("application/json");
                    response.setCharacterEncoding("utf-8");
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    PrintWriter writer = response.getWriter();
                    JSONObject result = new JSONObject();
                    result.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                    result.put("msg", "非法token");
                    writer.write(result.toJSONString());
                    writer.flush();
                    writer.close();
                    return;
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                exception.printStackTrace();
            }

            SysUser userInfo = userInfoFromToken.getUserInfo();

            if (userInfo != null) {
                UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(userInfo.getUsername(), null, userInfo.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authResult);
                chain.doFilter(request, response);
            } else {
                try {
                    response.setContentType("application/json");
                    response.setCharacterEncoding("utf-8");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    PrintWriter writer = response.getWriter();
                    JSONObject result = new JSONObject();
                    result.put("code", HttpServletResponse.SC_FORBIDDEN);
                    result.put("msg", "请登录");
                    writer.write(result.toJSONString());
                    writer.flush();
                    writer.close();
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
            }
        }
    }

}

该类主要覆写了BasicAuthenticationFilter中的doFilterInternal()方法,顾名思义,这个Filter在Security中承担基础验证工作。


项目运行截图


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!