美文网首页
Spring Security的项目中集成JWT Token令牌

Spring Security的项目中集成JWT Token令牌

作者: 在中国喝Java | 来源:发表于2022-12-26 14:03 被阅读0次

    引言

    最近接了一个私活项目,后台使用的是Spring Boot脚手架搭建的,认证和鉴权框架用的Spring Security。同时为了确保客户端安全访问后台服务的API,需要用户登录成功之后返回一个包含登录用户信息的jwt token, 用于调用其他接口时将此jwt token携带在请求头中作为调用者的认证信息。最近一个多月一方面在忙着做这个项目,另一方面恰好遇上了精彩的世界杯,也没怎么发文了。很多时候真的深感写篇原创文章比单纯的敲代码麻烦多了,但是好久不更文还是要检讨一下自己的惰性,客服自身的惰性是每个想要突破自我、不甘平庸的普通人的一辈子都不能松懈的重任。

    JWT 简介

    首先,让我们来补一下jwt的知识。jwt token 的全称叫JSON Web Token ,主要用于在各方之间以JSON 对象方式安全地传输信息。此信息是数字签名的,可以验证和信任,JWT 可以使用密钥(使用 HMAC 算法)或使用 RSAECDSA 的公钥/私钥对进行签名。

    虽然 JWT 可以加密以在各方之间提供保密性,但我们将专注于签名令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌会向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,只有持有私钥的一方才可以签署。

    jwt token 的适用场景

    • 鉴权(Authorization):这是最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。 单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。

    • 信息交换(Information Exchange):JWT令牌是在各方之间安全传输信息的好方法。 因为可以对 JWT 进行签名(例如,使用公钥/私钥对),所以可以确定发件人就是他们所说的那个人。此外,由于使用 headerpayload 计算签名,还可以验证内容是否被篡改。

    jwt 的结构

    JWT 由headerpayloadsignature三部分组成,以 . 分割:

    • header:通常由令牌的类型(即 JWT)以及正在使用的签名算法(例如 HMAC SHA256 或 RSA)两部分组成;

      {
      "alg": "HS256",
      "typ": "JWT"
      }
      复制代码
      

      然后,这个 JSON 被 Base64Url 编码以形成 JWT 的第一部分。

    • payload: 有效负载。其中包含声明,声明是关于实体(通常是用户)和附加数据的陈述。 声明分为三种类型:registered, public, private claims.

      注册(registered)声明:这是一组预定义的声明,不是强制性的,但建议使用。以提供一组有用的、可互操作的声明。比如:issue(发行人)、expired(到期时间)、subject(主题)、aud(受众)等。

      公共(public)声明:这些可以由使用人随意定义。 但是为了避免冲突,应该在jwt token 注册中定义,或者定义为包含抗冲突命名空间的 URI。

      私有(private)声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公共声明。

      示例如下:

      {
        "sub": "1234567890",
        "name": "John Doe",
        "iat": 1516239022
      }
      复制代码
      

      然后对有效负载进行 Base64Url 编码以形成 JSON Web 令牌的第二部分

      注意,对于已签名的令牌,此信息虽然受到保护以防篡改,但任何人都可以读取。除非已加密,否则请勿将机密信息放入 JWT 的有效负载或标头元素中。

    • Signature: 要创建签名部分,必须获取已编码的标头(header)、编码的有效负载(payload)、密钥、header中指定的算法,并对其进行签名。

    例如,如果您想使用 HMAC SHA256 算法,签名将通过以下方式创建:

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)
    复制代码
    

    签名用于验证信息在传输过程中是否被篡改,并且在使用私钥签名令牌的情况下,它还可以验证 JWT 的发送者是否正确。

    完整jwt

    由三个 . 分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,相对于基于 XML 的标准(如 SAML)则更紧凑。

    下面显示了一个 JWT,该 JWT 具有前面介绍过的headerpayload编码,并使用密钥签名

    [图片上传失败...(image-f33efa-1672120935196)]

    我们可以在 jwt.io Debugger 网站来解码、验证和生成 JWT。

    [图片上传失败...(image-f69c98-1672120935196)]

    jwt 的使用方式

    在身份校验中,当用户成功登录,将返回一个 JSON Web Token。由于令牌是凭据,因此必须非常小心以防止出现安全问题。

    通常令牌需要设置一个过期时间,超过过期时间则令牌失效,需要置换新的令牌。

    由于缺乏安全性,不应该将敏感的会话数据存储在浏览器中。每当用户需要访问受保护的路由或资源时,用户代理应该发送jwt,通常在 Authorization header 中使用 Bearer 模式。 header 的内容应如下所示:

    Authorization: Bearer <token>
    复制代码
    

    某些情况下,这可以是一种无状态授权机制。服务器的受保护路由将检查 Authorization header 中是否存在有效的 JWT,如果存在,则允许用户访问受保护的资源。如果 JWT 包含必要的数据,则可能会减少查询数据库以进行某些操作的需要,尽管情况并非总是如此。

    如果 tokenAuthorization header,跨域 Cross-Origin Resource Sharing (CORS)不是问题,因为它不使用 cookies

    客户端获取jwt令牌访问受保护资源的具体流程

    [图片上传失败...(image-159f5c-1672120935196)]

    1) 用户在在客户端使用用户名/密码登录;

    2)服务端使用密钥生成一个JWT令牌;

    3)服务端将生存的jwt令牌返回给浏览器;

    4)用户拿到jwt 令牌放到Authentication参数对应的请求头中访问服务端受保护的资源和API;

    5)服务端校验签名,从jwt令牌中解析获取用户信息;

    6)服务端校验签名通过并从jwt令牌中解析出用户信息,则返回API的成功响应信息给客户端

    Spring Security 安全框架下使用jwt token

    在非spring security框架下的spring boot项目中使用jwt令牌鉴权,我们只需要新建一个拦截器或者Servlet过滤器解析jwt token信息就行了,解析成功就放行请求,解析失败则返回403权限不足信息就行了。但是在Spring Security 框架中本身就自动适配了很多个过滤器,并组成了一个过滤器链,因此我们也需要新建一个解析jwt token的过滤器加入过滤器链中才行。

    新建一个spring boot项目

    使用IDEA新建spring boot项目的同时添加一些必要的依赖jar包,如spring mvc、mysql驱动、druid数据源和fast-json及代码简洁工具lombok等

    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.7.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.bonus</groupId>
        <artifactId>bonus-backend</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <name>bonus-backend</name>
        <description>bonus-backend</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
           <!--spring web mvc依赖 -->
           <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--修改配置文件自动生效依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <!--druid 数据源依赖-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.2.8</version>
            </dependency>
            <!--阿里json工具依赖 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.79</version>
            </dependency>
            <!--mysql驱动包-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.16</version>
                <scope>runtime</scope>
            </dependency>
            <!--代码简洁工具lombok依赖-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
        </dependencies>
         <build>
                <plugins>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                    </plugin>
                </plugins>
            </build>
    </project>
    复制代码
    

    加入spring security 和 jwt 相关依赖项

    在项目的pom.xml文件的dependencies标签中加入

            <!--加解密依赖-->
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>1.11</version>
            </dependency>
            <!--持久层框架mybatis-plus依赖-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.1.2</version>
            </dependency>
           <!--spring security依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <version>2.2.7.RELEASE</version>
            </dependency>
            <!--jwt token依赖-->
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.7.0</version>
            </dependency>
    复制代码
    

    项目配置文件

    application-porperties

    server.servlet.context-path=/bonus
    spring.profiles.active=dev
    
    spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
    spring.jackson.time-zone=GMT+8
    
    # mybatis-plus config
    mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
    mybatis-plus.configuration.map-underscore-to-camel-case=true
    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    
    # twelve.zodiac
    twelve.zodiac.mouse=03,15,27,39
    twelve.zodiac.cow=02,14,26,38
    twelve.zodiac.tiger=01,13,25,37,49
    twelve.zodiac.rabbit=12,24,36,48
    twelve.zodiac.dragon=11,23,35,47
    twelve.zodiac.snake=10,22,34,46
    twelve.zodiac.horse=09,21,33,45
    twelve.zodiac.sheep=08,20,32,44
    twelve.zodiac.monkey=07,19,31,43
    twelve.zodiac.chicken=06,18,30,42
    twelve.zodiac.dog=05,17,29,41
    twelve.zodiac.pig=04,16,28,40
    
    复制代码
    

    application-dev.properties

    server.address=127.0.0.1
    server.port=8090
    
    spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.druid.url=jdbc:mysql://localhost:3306/bonus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    spring.datasource.druid.username=bonus_user
    spring.datasource.druid.password=tiger2022@
    spring.datasource.druid.validation-query=select 1 from dual
    #spring.datasource.druid.connect-properties
    
    # redis config
    spring.redis.client-name=redis-client
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.database=0
    复制代码
    

    日志打印配置

    log4j.properties

    log4j.rootLogger=DEBUG,stdout
    log4j.logger.com.baomidou.mybatisplus=DEBUG
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n 
    复制代码
    

    启动类

    @SpringBootApplication
    @EnableConfigurationProperties
    @EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true, jsr250Enabled=true)
    public class BonusBackendApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(BonusBackendApplication.class, args);
        }
    
    }
    复制代码
    

    启动类中除了加上@SpringBootApplication注解之外,还加上了开启配置属性生效的注解@EnableConfigurationProperties以及全局安全访问注解@EnableGlobalMethodSecurity进行动态权限校验

    JWT相关API

    用于生成jwt token 和从 jwt token中解析出用户信息的相关API都在com.auth0.jwt.JWTcom.auth0.jwt.JWTCreator两个类中。

    JWT 类中的API方法

    • public JWT(): JWT类的构造方法;

    • public static Builder create(): 创建jwt token的构建器, 返回对象为JWTCreator类中的静态内部类Builder

    • public DecodedJWT decodeJwt(String token): 解析jwt token方法

    • public static DecodedJWT decode(String token) : 静态解析jwt token方法

    • public static Verification require(Algorithm algorithm): 通过算法构造Verification对象静态方法, Verification类主要用来校验jwt令牌是否有效

    JWTCreator类中的API方法

    静态内部类Builder主要用于构造headerpayload中 的内容, 该静态类主要提供一些列withXXX方法用于指定相应的键值对内容,主要有一下API方法:

    • public JWTCreator.Builder withHeader(Map<String, Object> headerClaims): 构造header代表的键值对集合;
    • public JWTCreator.Builder withKeyId(String keyId): 指定令牌header中的kid的值;
    • public JWTCreator.Builder withIssuer(String issuer): 指定令牌发行者;
    • public JWTCreator.Builder withSubject(String subject): 指定令牌主题;
    • public JWTCreator.Builder withAudience(String... audience): 指定令牌受众,通过该方法可以将令牌授予有限数量的用户;
    • public JWTCreator.Builder withExpiresAt(Date expiresAt): 指定令牌过期日期;
    • public JWTCreator.Builder withNotBefore(Date notBefore): 指定令牌不能早于某个日期使用;
    • public JWTCreator.Builder withIssuedAt(Date issuedAt): 指定令牌签发日期;
    • public JWTCreator.Builder withJWTId(String jwtId): 指定令牌id;
    • public JWTCreator.Builder withClaim(String name, Boolean value): 指定payload中的键值对,值为布尔类型;
    • public JWTCreator.Builder withClaim(String name, Integer value): 指定payload中的键值对,值为Integer类型;
    • public JWTCreator.Builder withClaim(String name, Long value) : 指定payload中的键值对,值为Long类型;
    • public JWTCreator.Builder withClaim(String name, Double value): 指定payload中的键值对,值为Double类型;
    • public JWTCreator.Builder withClaim(String name, String value): 指定payload中的键值对,值为String类型;
    • public JWTCreator.Builder withClaim(String name, Date value): 指定payload中的键值对,值为Date类型;
    • public JWTCreator.Builder withArrayClaim(String name, String[] items): 指定payload中的键值对,值为String数组类型;
    • public JWTCreator.Builder withArrayClaim(String name, Integer[] items): 指定payload中的键值对,值为Integer数组类型;
    • public JWTCreator.Builder withArrayClaim(String name, Long[] items): 指定payload中的键值对,值为Long数组类型;
    • public String sign(Algorithm algorithm) : 签名方法,通过算法签名,得到完整的jwt token内容方法

    algorithm算法对象可通过静态方法Algorithem#HMAC256或者Algorithem#HMAC512方法创建,入参为一个String类型的密钥

    JWTDecoder类中的API方法

    JWTDecoder类为DecodedJWT类的实现类,主要用来从解析jwt令牌后的对象中获取想要的字段信息

    • public String getAlgorithm(): 获取签名算法名称;

    • public String getType(): 获取jwt令牌的类型,默认为jwt;

    • public String getKeyId(): 获取jwt 令牌header中的kid对应的值;

    • public Claim getHeaderClaim(String name): 获取header中指定名字的Claim, 它可以进一步把value代表的数据转成各种数据类型;

    • public String getIssuer(): 获取jwt令牌的签发人;

    • public String getSubject():获取jwt令牌的主题;

    • public List<String> getAudience(): 获取jwt 令牌的受众;

    • public Date getExpiresAt(): 获取jwt令牌过期时间;

    • public Date getNotBefore(): 获取令牌不能早于使用的时间;

    • public String getId(): 获取令牌id;

    • public Claim getClaim(String name): 获取指定名字Claim

    • public Map<String, Claim> getClaims(): 获取jwt令牌中的Claim键值对集合;

    • public String getHeader(): 获取jwt令牌中的header部分内容;

    • public String getPayload(): 获取jwt令牌中的payload部分内容;

    • public String getSignature(): 获取jwt 令牌中签名部分内容;

    • public String getToken(): 还原jwt令牌内容;

    新建Jwt令牌工具类

    利用JWT相关API我们新建了一个JwtTokenUtil的工具类用于生成jwt令牌

    public class JwtTokenUtil {
    
        // 密钥
        private static final String SECRET = "bonusBACKEND2022$";
    
        // 过期时间7天
        private static final int EXPIRE_SECONDS = 7*24*3600;
    
        private final static Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
    
        /**
         * 生成token方法
         * @param memInfoMap
         * @return jwtToken
         */
        public static String genAuthenticatedToken(Map<String, Object> memInfoMap){
            List<GrantedAuthority> authorities = (List<GrantedAuthority>) memInfoMap.get("authorities");
            String authorityStr = null;
            if(authorities!=null && authorities.size()>0){
                StringBuffer buffer = new StringBuffer();
                for(int i=0; i<authorities.size()-1; i++){
                    buffer.append(authorities.get(i).getAuthority()).append(",");
                }
                buffer.append(authorities.get(authorities.size()-1).getAuthority());
                authorityStr = buffer.toString();
            }
            String[] authorityArray = authorityStr!=null?authorityStr.split(","):null;
            Calendar nowTime = Calendar.getInstance();
            //过期时间
            nowTime.add(Calendar.SECOND, EXPIRE_SECONDS);
            Date expireDate = nowTime.getTime();
            String jwtToken = JWT.create().withJWTId(UUID.randomUUID().toString().replaceAll("-", ""))
                    .withClaim("memId", (Long) memInfoMap.get("memId"))
                    .withClaim("memAccount", (String) memInfoMap.get("memAccount"))
                    .withClaim("memPwd", (String) memInfoMap.get("memPwd"))
                    .withClaim("totalCreditAmount", ((BigDecimal) memInfoMap.get("totalCreditAmount")).doubleValue())
                    .withClaim("usedCreditAmount", ((BigDecimal) memInfoMap.get("usedCreditAmount")).doubleValue())
                    .withClaim("remainCreditAmount", ((BigDecimal) memInfoMap.get("remainCreditAmount")).doubleValue())
                    .withArrayClaim("authorities", authorityArray)
                    .withIssuedAt(new Date(System.currentTimeMillis()))
                    .withExpiresAt(expireDate)
                    .sign(Algorithm.HMAC256(SECRET));
            return jwtToken;
        }
    }
    复制代码
    

    实现用户认证方法

    UserDetailService#loadUserByUsername

    @Service
    public class MemInfoServiceImpl extends ServiceImpl<MemInfoMapper, MemInfoDTO> implements MemInfoService {
     private final static Logger logger = LoggerFactory.getLogger(MemInfoServiceImpl.class);
        @Resource
        private MyPasswordEncoder passwordEncoder;
        @Resource
        private RoleInfoService roleInfoService;
    
        @Override
        @Transactional
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            MemInfoDTO memInfoDTO = this.baseMapper.getMemInfoByAccount(username);
            if(memInfoDTO==null){
                throw  new UsernameNotFoundException("Username" + username + "is invalid!");
            }
            // 获取用户角色列表
            List<RoleInfoDTO> roleInfoDTOList = roleInfoService.getRolesByMemId(memInfoDTO.getMemId());
            if(roleInfoDTOList.size()>0){
                for(RoleInfoDTO roleInfoDTO: roleInfoDTOList){
                    SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + roleInfoDTO.getRoleName().toUpperCase());
                    memInfoDTO.getAuthorities().add(grantedAuthority);
                }
            }
            return memInfoDTO;
        }
    复制代码
    

    MemInfoDTO类源码如下:

    @Data
    @TableName("bonus_mem_info")
    @ApiModel(value="MemInfoDTO", description = "会员DTO")
    @Validated
    public class MemInfoDTO extends BaseDTO implements UserDetails {
    
        /**
         * 会员id
         */
        @TableId
        @ApiModelProperty(name = "memId", value = "memId", notes = "会员ID", dataType = "Long")
        private Long memId;
    
        /**
         * 会员账号
         */
        @TableField(value = "mem_account")
        @NotEmpty(message = "会员账号不能为空")
        @ApiModelProperty(name="memAccount", value = "memAccount", notes = "会员账号", dataType = "String")
        private String memAccount;
    
        /**
         * 会员密码
         */
        @TableField(value = "mem_pwd")
        @NotEmpty(message = "会员密码不能为空")
        @ApiModelProperty(name="memPwd", value = "memPwd", notes = "加密后的会员密码", dataType = "String")
        private String memPwd;
    
        /**
         * 会员类型:1-vip;2-代理
         */
        @TableField(value = "mem_type")
        @NotEmpty(message = "会员类型不能为空")
        @ApiModelProperty(name="memType", value = "memType", notes = "会员类型", dataType = "Integer", example = "1", allowableValues = "1,2")
        private Integer memType;
    
        /**
         * 会员信用额度,单位分
         */
        @TableField(value = "total_credit_amount")
        @NotEmpty(message = "会员信用额度不能为空")
        @ApiModelProperty(name = "totalCreditAmount", value = "totalCreditAmount", notes = "会员总信用额度,单位分", dataType = "Long", example = "10000")
        private Long totalCreditAmount;
    
        /**
         * 会员已使用信用额度,单位分
         */
        @ApiModelProperty(name = "usedCreditAmount", value = "usedCreditAmount", notes = "会员已使用信用额度,单位分", dataType = "Long", example = "5000")
        @TableField(value = "used_credit_amount")
        private Long usedCreditAmount;
    
        @TableField(exist = false)
        private List<GrantedAuthority> authorities = new ArrayList<>();
    
        @Override
        public Collection<GrantedAuthority> getAuthorities() {
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return this.memPwd;
        }
    
        @Override
        public String getUsername() {
            return this.memAccount;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    复制代码
    

    新建JwtToken认证过滤器

    public class JwtAuthenticationFilterBean extends GenericFilterBean {
    
        private final static Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilterBean.class);
    
        private String AUTHORIZATION_NAME = "Authorization";
    
        private String BEARER = "Bearer";
    
        private static List<String> whiteRequestList = new ArrayList<>();
    
        static {
            whiteRequestList.add("/bonus/member/checkSafetyCode");
            whiteRequestList.add("/bonus/login");
            whiteRequestList.add("/bonus/member/login");
            whiteRequestList.add("/bonus/common/kaptcha");
            whiteRequestList.add("/bonus/admin/login");
            whiteRequestList.add("/bonus/favicon.ico");
            whiteRequestList.add("/bonus/doc.html");
            whiteRequestList.add("/bonus/error");
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            logger.info("requestUrl="+request.getRequestURI());
            if(whiteRequestList.contains(request.getRequestURI()) || (request.getRequestURI().contains("admin/dist") &&
                    request.getRequestURI().endsWith(".css") || request.getRequestURI().equals(".js") ||
                    request.getRequestURI().endsWith(".png") || request.getRequestURI().endsWith("favicon.ico"))){
                // 如果是登录和安全码验证请求直接放行
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            } else {
                   String bearerToken = request.getHeader(AUTHORIZATION_NAME);
                   if(StringUtils.isEmpty(bearerToken)||!bearerToken.startsWith(BEARER)){
                       printException(response, HttpStatus.UNAUTHORIZED.value(), "缺失jwt令牌或令牌格式错误");
                       return;
                   }
                   String authToken = bearerToken.substring(bearerToken.indexOf(BEARER)+BEARER.length()+1);
                   if(StringUtils.isEmpty(authToken)){
                       String message = "http header Authorization is null, user Unauthorized";
                       response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                       response.setStatus(HttpStatus.UNAUTHORIZED.value());
                       this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                       return;
                   } else {
                       try {
                           DecodedJWT decodedJWT = JWT.decode(authToken);
                           Map<String, Claim> claimMap = decodedJWT.getClaims();
                           Claim expireClaim = claimMap.get("exp");
                           Date expireDate = expireClaim.asDate();
                           // 校验token 是否过期
                           if(expireDate.before(DateUtil.date(System.currentTimeMillis()))){
                               String message = "Authorization token expired";
                               this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                               return;
                           }
                           Claim memAccountClaim = claimMap.get("memAccount");
                           if(memAccountClaim==null || StringUtils.isEmpty(memAccountClaim.asString())){
                               String message = "memAccount cannot be null";
                               response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                               response.setStatus(HttpStatus.UNAUTHORIZED.value());
                               this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                               return;
                           }
                           // 请求头认证通过, 放行请求
                           filterChain.doFilter(servletRequest, servletResponse);
                       } catch (JWTDecodeException e) {
                           String message = "JWT decode authToken failed, caused by " + e.getMessage();
                           this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                           return;
                       }
                   }
            }
    
        }
    
        /**
         * 打印请求头认证失败信息
         * @param response
         * @param status
         * @param message
         * @throws IOException
         */
        private void printException(HttpServletResponse response, int status, String message) throws IOException {
            logger.error(message);
            response.setStatus(status);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            PrintWriter printWriter = response.getWriter();
            ResponseResult<String> responseResult = ResponseResult.error(status, message);
            printWriter.write(JSONObject.toJSONString(responseResult));
            printWriter.flush();
            printWriter.close();
        }
    }
    复制代码
    

    Spring Security配置类中配置登录成功后返回jwt令牌

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final static Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
    
        @Resource
        private MemInfoService memInfoService;
    
        private MathContext mathContext = new MathContext(2, RoundingMode.HALF_UP);
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            super.configure(auth);
            auth.userDetailsService(memInfoService);
        }
    
        @Override
        public void configure(WebSecurity web) {
            web.ignoring().antMatchers("/static/**","/index.html","/templates/**", "/admin/**", "/doc.html", "/webjars/**", "/v2/*", "/favicon.ico", "/swagger-resources");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            JwtAuthenticationFilterBean jwtAuthenticationFilterBean = new JwtAuthenticationFilterBean();
            http.addFilterBefore(jwtAuthenticationFilterBean, UsernamePasswordAuthenticationFilter.class); // 将JwtToken认证过滤器注册在登录认证过滤器之前
            // 配置跨域
            http.cors().configurationSource(corsConfigurationSource())
                    .and().logout().invalidateHttpSession(true).logoutUrl("/member/logout").permitAll()
            ;
            http.authorizeRequests().antMatchers("/member/checkSafetyCode").permitAll()
                    .antMatchers("/doc.html").permitAll()
                    .antMatchers("/common/kaptcha").permitAll()
                    .antMatchers("/admin/login").permitAll()
                    .anyRequest().authenticated()
                    .and().httpBasic()
                    .and().formLogin()
                    .loginProcessingUrl("/member/login") // 登录接口
                    .successHandler((httpServletRequest, httpServletResponse, authentication) -> {
                         httpServletResponse.setContentType("application/json;charset=utf-8");
                         httpServletResponse.setStatus(HttpStatus.OK.value());
                         PrintWriter printWriter = httpServletResponse.getWriter();
                         MemInfoDTO memInfoDTO = (MemInfoDTO) authentication.getPrincipal();
                         Map<String, Object> userMap = new HashMap<>();
                         userMap.put("memId", memInfoDTO.getMemId());
                         userMap.put("memAccount", memInfoDTO.getMemAccount());
                         userMap.put("memPwd", memInfoDTO.getMemPwd());
                         BigDecimal totalCredit = memInfoDTO.getTotalCreditAmount()!=null?new BigDecimal(memInfoDTO.getTotalCreditAmount()/100, mathContext): new BigDecimal("0.0");
                         userMap.put("totalCreditAmount", totalCredit);
                         BigDecimal usedCredit = memInfoDTO.getUsedCreditAmount()!=null?new BigDecimal(memInfoDTO.getUsedCreditAmount()/100, mathContext):new BigDecimal("0.0");
                         userMap.put("usedCreditAmount", usedCredit);
                         Long remainCredit = (memInfoDTO.getTotalCreditAmount()==null?0:memInfoDTO.getTotalCreditAmount()) - (memInfoDTO.getUsedCreditAmount()==null?0:memInfoDTO.getUsedCreditAmount());
                         BigDecimal remainCreditAmount = new BigDecimal(remainCredit/100, mathContext);
                         userMap.put("remainCreditAmount", remainCreditAmount);
                         userMap.put("authorities", memInfoDTO.getAuthorities());
                         Map<String, Object> dataMap = new HashMap<>();
                         dataMap.put("memInfo", userMap);
                         dataMap.put("authenticatedToken", "Bearer "+JwtTokenUtil.genAuthenticatedToken(userMap));
                         ResponseResult<Map<String, Object>> responseResult = ResponseResult.success(dataMap, "login success");
                         printWriter.write(JSONObject.toJSONString(responseResult));
                         printWriter.flush();
                         printWriter.close();
                    }).failureHandler((httpServletRequest, httpServletResponse, e) -> {
                         logger.error("login failed, caused by " + e.getMessage());
                         httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
                         httpServletResponse.setStatus(HttpStatus.OK.value());
                         PrintWriter printWriter = httpServletResponse.getWriter();
                         ResponseResult<String> responseResult = ResponseResult.error(HttpStatus.UNAUTHORIZED.value(), "authentication failed");
                         responseResult.setPath(httpServletRequest.getRequestURI());
                         printWriter.write(JSONObject.toJSONString(responseResult));
                         printWriter.flush();
                         printWriter.close();
                    }).permitAll()
                    .and().csrf().disable().exceptionHandling().accessDeniedHandler(accessDeniedHandler());
    
        }
    
        //配置跨域访问资源
        private CorsConfigurationSource corsConfigurationSource() {
            UrlBasedCorsConfigurationSource source =   new UrlBasedCorsConfigurationSource();
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.addAllowedOrigin("*");    //同源配置,*表示任何请求都视为同源,若需指定ip和端口可以改为如“localhost:8080”,多个以“,”分隔;
            corsConfiguration.addAllowedHeader("*");//header,允许哪些header,本案中使用的是token,此处可将*替换为token;
            corsConfiguration.addAllowedMethod("*");    //允许的请求方法,PSOT、GET等
            corsConfiguration.setAllowCredentials(true);
            // 注册跨域配置
            source.registerCorsConfiguration("/**",corsConfiguration); //配置允许跨域访问的url
            return source;
        }
    
        @Bean
        AccessDeniedHandler accessDeniedHandler() {
            return new AuthenticationAccessDeniedHandler();
        }
    }
    复制代码
    

    测试效果

    在启动类中运行Main方法运行服务后就可以测试效果了

    测试生成jwt令牌

    我们首先测试生成jwt token的登录接口, 在postman中调用登录接口

    post http://localhost:8090/bonus/member/login??username=zhangsan&password=zhangsan1234

    接口返回信息如下:

    {
        "code": 200,
        "data": {
            "memInfo": {
                "memAccount": "zhangsan",
                "totalCreditAmount": 2000,
                "memPwd": "82dea760d7bb362ca74883836ee4d6ba",
                "remainCreditAmount": 2000,
                "usedCreditAmount": 0,
                "authorities": [
                    {
                        "authority": "ROLE_USER"
                    }
                ],
                "memId": 1592927262097924097
            },
            "authenticatedToken": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZW1BY2NvdW50IjoiemhhbmdzYW4iLCJ0b3RhbENyZWRpdEFtb3VudCI6MjAwMC4wLCJtZW1Qd2QiOiI4MmRlYTc2MGQ3YmIzNjJjYTc0ODgzODM2ZWU0ZDZiYSIsInJlbWFpbkNyZWRpdEFtb3VudCI6MjAwMC4wLCJ1c2VkQ3JlZGl0QW1vdW50IjowLjAsImV4cCI6MTY3MjU1ODAyMSwiaWF0IjoxNjcxOTUzMjIxLCJqdGkiOiI2M2M1YmExZDIzZGY0YjIzODQ1NWU5YjkwNzQzMzRmMSIsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJtZW1JZCI6MTU5MjkyNzI2MjA5NzkyNDA5N30.S5UQLasL-SALKBHwhhUk_DGv__YPlRJQ7TC1pBzxb0g"
        },
        "message": "login success"
    }
    复制代码
    

    memPwd字段为密码加密后的密文

    authenticatedToken 对应的内容为Bearer模式的jwt令牌, 真正的jwt令牌内容为eyj开头的那串较长的字符串。

    测试通过jwt令牌认证与鉴权

    新建一个获取配置数据的接口

    @RestController
    @RequestMapping("/config")
    public class ConfigController {
    
        @Resource
        private ZodiacProperties zodiacProperties;
    
        @GetMapping("/twelve/zodiacs")
        public ResponseResult<ZodiacProperties> getTwelveZodiacs(){
    
            return ResponseResult.success(zodiacProperties);
        }
    
    }
    复制代码
    

    ZodiacProperties类源码如下:

    @Component
    @ConfigurationProperties(prefix = "twelve.zodiac")
    public class ZodiacProperties {
    
        private String mouse;
    
        private String cow;
    
        private String tiger;
    
        private String rabbit;
    
        private String dragon;
    
        private String snake;
    
        private String horse;
    
        private String sheep;
    
        private String monkey;
    
        private String chicken;
    
        private String dog;
    
        private String pig;
        // 省略set、get方法
    }
    复制代码
    

    接口写好后,重启后台服务,并重新登录拿到jwt令牌令牌

    首先试一下不在请求头中加入jwt令牌的结果

    GET http://localhost:8090/bonus/config/twelve/zodiacs

    接口返回结果:

    {
        "code": 401,
        "message": "缺失jwt令牌或令牌格式错误"
    }
    复制代码
    

    然后在请求头中加入Authentication参数jwt令牌再次测试结果

    [图片上传失败...(image-acef6d-1672120935194)]

    此时返回结果:

    {
        "code": 200,
        "message": "ok",
        "path": null,
        "data": {
            "mouse": "03,15,27,39",
            "cow": "02,14,26,38",
            "tiger": "01,13,25,37,49",
            "rabbit": "12,24,36,48",
            "dragon": "11,23,35,47",
            "snake": "10,22,34,46",
            "horse": "09,21,33,45",
            "sheep": "08,20,32,44",
            "monkey": "07,19,31,43",
            "chicken": "06,18,30,42",
            "dog": "05,17,29,41",
            "pig": "04,16,28,40"
        }
    }
    复制代码
    

    关于如何在集成spring security安全访问框架的spring boot项目中如何使用jwt令牌安全访问服务端API就讲到这里,需要项目源码的读者朋友关注笔者的微信公众号【阿福谈Web编程】,并发送关键字信息【bonus-backend】即可获得项目源码下载地址。

    作者:heshengfu1211
    链接:https://juejin.cn/post/7181109766742081592
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

          本文标题:Spring Security的项目中集成JWT Token令牌

          本文链接:https://www.haomeiwen.com/subject/muynqdtx.html