美文网首页Spring Cloud Java编程javaWeb学习
SpringBoot系列之前后端接口安全技术JWT

SpringBoot系列之前后端接口安全技术JWT

作者: smileNicky | 来源:发表于2020-07-10 14:11 被阅读0次

    @TOC

    1. 什么是JWT?

    JWT的全称为Json Web Token (JWT),是目前最流行的跨域认证解决方案,是在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519),JWT 是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权

    引用官方的说法是:

    JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以进行验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。

    引用官网图片,JWT生成的token格式如图:


    在这里插入图片描述

    2. JWT令牌结构怎么样?

    JSON Web令牌以紧凑的形式由三部分组成,这些部分由点(.)分隔,分别是:

    • 标头(Header)
    • 有效载荷(Playload)
    • 签名(Signature)
      因此,JWT通常如下所示。
      xxxxx.yyyyy.zzzzz
      在这里插入图片描述

    ok,详细介绍一下这3部分组成

    2.1 标头(Header)

    标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。
    * 声明类型,这里是JWT
    * 加密算法,自定义

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    然后进行Base64Url编码得到jwt的第1部分

    Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2
    的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24
    个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中 提
    供了非常方便的 B BA AS SE E6 64 4E En nc co od de er r和B BA AS SE E6 64 4D De ec co od de er r,用它们可以非常方便的完
    成基于 BASE64 的编码和解码

    2.2 有效载荷(Playload)

    载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包
    含三个部分:

    • (1)标准中注册的声明

      • iss (issuer):表示签发人
      • exp (expiration time):表示token过期时间
      • sub (subject):主题
      • aud (audience):受众
      • nbf (Not Before):生效时间
      • iat (Issued At):签发时间
      • jti (JWT ID):编号
    • (2)公共的声明
      公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息

    • (3)私有的声明
      私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。这些私有的声明其实一般就是指自定义Claim

    定义一个payload:

    {
        "user_id":1,
        "user_name":"nicky",
        "scope":[
            "ROLE_ADMIN"
        ],
        "non_expired":false,
        "exp":1594352348,
        "iat":1594348748,
        "enabled":true,
        "non_locked":false
    }
    

    对其进行base64加密,得到payload:

    eyJ1c2VyX2lkIjoxLCJ1c2VyX25hbWUiOiJuaWNreSIsInNjb3BlIjpbIlJPTEVfQURNSU4iXSwibm9uX2V4cGlyZWQiOmZhbHNlLCJleHAiOjE1OTQzNTIzNDgsImlhdCI6MTU5NDM0ODc0OCwiZW5hYmxlZCI6dHJ1ZSwibm9uX2xvY2tlZCI6ZmFsc2V9
    

    2.3 签名(Signature)

    jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

    • header (base64后的)
    • payload (base64后的)
    • secret
      签名,是整个数据的认证信息。一般根据前两步的数据,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第3部分

    ok,一个jwt令牌的组成就介绍好咯,令牌是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。
    下图显示了一个JWT,它已对先前的标头和有效负载进行了编码,并用一个秘密secret进行了签名编码的JWT:


    在这里插入图片描述

    JWT官网提供的在线调试工具:
    https://jwt.io/#debugger-io

    在这里插入图片描述
    开源中国提供的base64在线加解密:
    https://tool.oschina.net/encrypt?type=3
    在这里插入图片描述

    3. JWT原理简单介绍

    引用官网的图,用于显示如何获取JWT,并将其用于访问API或资源:

    在这里插入图片描述
    • 1、客户端(包括浏览器、APP等)向授权服务器请求授权
    • 2、授权服务器验证通过,授权服务器会向应用程序返回访问令牌
    • 3、该应用程序使用访问令牌来访问受保护的资源(例如API)

    4. JWT的应用场景

    JWT 使用于比较小型的业务验证,对于比较复杂的可以用OAuth2.0实现

    引用官方的说法:

    • 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
    • 信息交换:JSON Web令牌是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

    5. 与Cookie-Session对比

    了解JWT之前先要了解传统的Cookie-Session认证机制,这是单体应用最常用的,其大概流程:

    • 1、用户访问客户端(浏览器),服务器通过session校验用户是否登录
    • 2、 用户没登录返回登录页面,输入账号密码等验证
    • 3、 验证通过创建session,返回sessionId给客户端保存到cookie
    • 4、接着,用户访问其它同域链接,都会校验sessionId,符合就允许访问

    ok,简单介绍这套cookie-session机制,之前设计者开发这套机制是为了兼容http的无状态,这套机制有其优点,当然也有一些缺陷:

    • 只适用于B/S架构的软件,对于安卓app等客户端不带cookie的,不能和服务端进行对接
    • 不支持跨域,因为Cookie为了保证安全性,只能允许同域访问,不支持跨域
    • CSRF攻击,Cookie没做好安全保证,有时候容易被窃取,受到跨站请求伪造的攻击

    ok,简单介绍了cookie-session机制后,可以介绍一下jwt的认证

    • 1、用户访问客户端(浏览器、APP等等),服务器通过token校验
    • 2、 用户没登录返回登录页面,输入账号密码等验证
    • 3、 验证通过创建已签名token,返回token给客户端保存,最常见的是存储在localStorage中,但是也可以存在Session Storage和Cookie中
    • 4、接着,用户访问其它链接,都会带上token,服务器解码JWT,如果Token是有效的则处理这个请求

    网上对于cookie-session机制和jwt的讨论很多,可以自行网上找资料,我觉得这两套机制各有优点,应该根据场景进行选用,JWT最明显优点就是小巧轻便,安全性也比较好,但是也有其缺点。

    • 比如对于业务繁杂的功能,如果一些信息也丢在jwt的token里,cookie有可能不能保存。
    • 续签问题,jwt不能支持,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题
    • 密码重置等问题,jwt因为数据不保存于服务端,如果用户修改密码,不过token还没过期,这种情况,原来的token还是可以访问系统的,这种肯定是不允许的,不过这种情况或许可以通过修改secret实现

    6. Java的JJWT实现JWT

    6.1 什么是JJWT?

    JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache
    License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界
    面,隐藏了它的大部分复杂性。

    6.2 实验环境准备

    环境准备:

    • Maven 3.0+
    • IntelliJ IDEA

    技术栈:

    • SpringBoot2.2.1
    • Spring Security

    新建一个SpringBoot项目,maven加入JJWT相关配置

    <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>${jjwt.version}</version>
            </dependency>
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>${java.jwt.version}</version>
            </dependency>
    

    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 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.1.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example.springboot</groupId>
        <artifactId>springboot-jwt</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>springboot-jwt</name>
        <description>Demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
            <jjwt.version>0.9.0</jjwt.version>
            <java.jwt.version>3.4.0</java.jwt.version>
            <mybatis.springboot.version>2.1.1</mybatis.springboot.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
    
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>${jjwt.version}</version>
            </dependency>
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>${java.jwt.version}</version>
            </dependency>
    
            <!-- springboot mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.springboot.version}</version>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.27</version>
                <scope>runtime</scope>
            </dependency>
    
            <!-- SpringBoot thymeleaf-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.47</version>
                <scope>compile</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
    

    application.yml:

    spring:
      datasource:
        url: jdbc:mysql://192.168.0.152:33306/jeeplatform?autoReconnect=true&useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=false
        username: root
        password: minstone
        driver-class-name: com.mysql.jdbc.Driver
      #添加Thymeleaf配置,除了cache在项目没上线前建议关了,其它配置都可以不用配的,本博客只是列举一下有这些配置
      thymeleaf:
        # cache默认开启的,这里可以关了,项目上线之前,项目上线后可以开启
        cache: false
        # 这个prefix可以注释,因为默认就是templates的,您可以改成其它的自定义路径
        prefix: classpath:/templates/
        suffix: .html
        mode: HTML5
        # 指定一下编码为utf8
        encoding: UTF-8
        # context-type为text/html,也可以不指定,因为boot可以自动识别
        servlet:
          content-type: text/html
      messages:
        basename: i18n.messages
        #    cache-duration:
        encoding: UTF-8
    
    
    logging:
      level:
        org:
          springframework:
            security: DEBUG
        com:
          example:
            springboot:
              jwt:
                mapper: DEBUG
    

    项目工程:


    在这里插入图片描述

    6.3 jwt配置属性读取

    新建jwt.yml:

    # jwt configuration
    jwt:
      # 存放Token的Header key值
      token-key: Authorization
      # 自定义密钥,加盐
      secret: mySecret
      # 超时时间 单位秒
      expiration: 3600
      # 自定义token 前缀字符
      token-prefix: Bearer-
      # accessToken超时时间 单位秒
      access-token: 3600
      # 刷新token时间 单位秒
      refresh-token: 3600
      # 允许访问的uri
      permit-all: /oauth/**,/login/**,/logout/**
      # 需要校验的uri
      authenticate-uri: /api/**
    
    

    JWTProperties .java

    package com.example.springboot.jwt.configuration;
    
    import com.example.springboot.jwt.core.io.support.YamlPropertyResourceFactory;
    import lombok.Data;
    import lombok.ToString;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.stereotype.Component;
    
    import java.time.Duration;
    
    /**
     * <pre>
     *  JWT配置类
     * </pre>
     *
     * <pre>
     * @author nicky.ma
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/07/06 11:37  修改内容:
     * </pre>
     */
    @Component
    @PropertySource(value = "classpath:jwt.yml",encoding = "utf-8",factory = YamlPropertyResourceFactory.class)
    @ConfigurationProperties(prefix = "jwt")
    @Data
    @ToString
    public class JWTProperties {
    
        /**
         * 存放Token的Header key值
         */
        private String tokenKey;
    
        /*
         * 自定义密钥,加盐
         */
        private String secret;
    
        /*
         * 超时时间 单位秒
         */
        private Duration expiration =Duration.ofMinutes(3600);
    
        /*
         * 自定义token 前缀字符
         */
        private String tokenPrefix;
    
        /*
         * accessToken超时时间 单位秒
         */
        private Duration accessToken =Duration.ofMinutes(3600);
    
        /*
         * 刷新token时间 单位秒
         */
        private Duration refreshToken =Duration.ofMinutes(3600);
    
        /*
         * 允许访问的uri
         */
        private String permitAll;
    
        /*
         * 需要校验的uri
         */
        private String authenticateUri;
    }
    
    

    SpringBoot2.2.1版本使用@ConfigurationProperties注解是不能读取yaml文件的,只能读取properties,所以自定义PropertySourceFactory

    package com.example.springboot.jwt.core.io.support;
    
    import org.springframework.boot.env.YamlPropertySourceLoader;
    import org.springframework.core.env.PropertySource;
    import org.springframework.core.io.support.DefaultPropertySourceFactory;
    import org.springframework.core.io.support.EncodedResource;
    import org.springframework.core.io.support.PropertySourceFactory;
    import org.springframework.lang.Nullable;
    
    import java.io.IOException;
    import java.util.List;
    import java.util.Optional;
    
    /**
     * <pre>
     *  YAML配置文件读取工厂类
     * </pre>
     * <p>
     * <pre>
     * @author nicky.ma
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2019/11/13 15:44  修改内容:
     * </pre>
     */
    public class YamlPropertyResourceFactory implements PropertySourceFactory {
    
        /**
         * Create a {@link PropertySource} that wraps the given resource.
         *
         * @param name     the name of the property source
         * @param encodedResource the resource (potentially encoded) to wrap
         * @return the new {@link PropertySource} (never {@code null})
         * @throws IOException if resource resolution failed
         */
        @Override
        public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource encodedResource) throws IOException {
            String resourceName = Optional.ofNullable(name).orElse(encodedResource.getResource().getFilename());
            if (resourceName.endsWith(".yml") || resourceName.endsWith(".yaml")) {
                //yaml资源文件
                List<PropertySource<?>> yamlSources = new YamlPropertySourceLoader().load(resourceName, encodedResource.getResource());
                return yamlSources.get(0);
            } else {
                //返回默认的PropertySourceFactory
                return new DefaultPropertySourceFactory().createPropertySource(name, encodedResource);
            }
        }
    }
    
    

    6.4 JWT Token工具类

    package com.example.springboot.jwt.core.jwt.util;
    
    import com.alibaba.fastjson.JSON;
    import com.example.springboot.jwt.configuration.JWTProperties;
    import com.example.springboot.jwt.core.jwt.userdetails.JWTUserDetails;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    
    import java.util.*;
    
    
    /**
     * <pre>
     *   JWT工具类
     * </pre>
     *
     * <pre>
     * @author mazq
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/07/06 13:57  修改内容:
     * </pre>
     */
    @Component
    @Slf4j
    public class JWTTokenUtil {
    
        private static final String CLAIM_KEY_USER_ID = "user_id";
        private static final String CLAIM_KEY_USER_NAME ="user_name";
        private static final String CLAIM_KEY_ACCOUNT_ENABLED = "enabled";
        private static final String CLAIM_KEY_ACCOUNT_NON_LOCKED = "non_locked";
        private static final String CLAIM_KEY_ACCOUNT_NON_EXPIRED = "non_expired";
        private static final String CLAIM_KEY_AUTHORITIES = "scope";
        //签名方式
        private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
    
    
        @Autowired
        JWTProperties jwtProperties;
    
        /**
         * 生成acceptToken
         * @param userDetails
         * @return
         */
        public String generateToken(UserDetails userDetails) {
            JWTUserDetails user = (JWTUserDetails) userDetails;
            Map<String, Object> claims = generateClaims(user);
            return generateToken(user.getUsername(),claims);
        }
    
        /**
         * 生成acceptToken
         * @param username
         * @param claims
         * @return
         */
        public String generateToken(String username, Map<String, Object> claims) {
            return Jwts.builder()
                    .setId(UUID.randomUUID().toString())
                    .setSubject(username)
                    .setClaims(claims)
                    .setIssuedAt(new Date())
                    .setExpiration(generateExpirationDate(jwtProperties.getExpiration().toMillis()))
                    .signWith(SIGNATURE_ALGORITHM, jwtProperties.getSecret())
                    .compact();
        }
    
        /**
         * 校验acceptToken
         * @param token
         * @param userDetails
         * @return
         */
        public boolean validateToken(String token, UserDetails userDetails) {
            JWTUserDetails user = (JWTUserDetails) userDetails;
           return validateToken(token, user.getUsername());
        }
    
        /**
         * 校验acceptToken
         * @param token
         * @param  username
         * @return
         */
        public boolean validateToken(String token, String username) {
            try {
                final String userId = getUserIdFromClaims(token);
                return getClaimsFromToken(token) != null
                        && userId.equals(username)
                        && !isTokenExpired(token);
            } catch (Exception e) {
                throw new IllegalStateException("Invalid Token!"+e);
            }
        }
    
        /**
         * 校验acceptToken
         * @param token
         * @return
         */
        public boolean validateToken(String token) {
            try {
                return getClaimsFromToken(token) != null
                        && !isTokenExpired(token);
            } catch (Exception e) {
                throw new IllegalStateException("Invalid Token!"+e);
            }
        }
    
        /**
         * 解析token 信息
         * @param token
         * @return
         */
        public Claims  getClaimsFromToken(String token){
            Claims claims = Jwts.parser()
                        .setSigningKey(jwtProperties.getSecret())
                        .parseClaimsJws(token)
                        .getBody();
            return claims;
        }
    
        /**
         * 从token获取userId
         * @param token
         * @return
         */
        public String getUserIdFromClaims(String token) {
            String userId = getClaimsFromToken(token).getId();
            return userId;
        }
    
        /**
         * 从token获取ExpirationDate
         * @param token
         * @return
         */
        public Date getExpirationDateFromClaims(String token) {
            Date expiration = getClaimsFromToken(token).getExpiration();
            return expiration;
        }
    
        /**
         * 从token获取username
         * @param token
         * @return
         */
        public String getUsernameFromClaims(String token) {
            return  getClaimsFromToken(token).get(CLAIM_KEY_USER_NAME).toString();
        }
    
        /**
         * token 是否过期
         * @param token
         * @return
         */
        public boolean isTokenExpired(String token) {
            final Date expirationDate = getExpirationDateFromClaims(token);
            return expirationDate.before(new Date());
        }
    
        /**
         * 生成失效时间
         * @param expiration
         * @return
         */
        public Date generateExpirationDate(long expiration) {
            return new Date(System.currentTimeMillis() + expiration * 1000);
        }
    
        /**
         * 生成Claims
         * @Param user
         * @return
         */
        public Map<String, Object> generateClaims(JWTUserDetails user) {
            Map<String, Object> claims = new HashMap<>(16);
            claims.put(CLAIM_KEY_USER_ID, user.getUserId());
            claims.put(CLAIM_KEY_USER_NAME, user.getUsername());
            claims.put(CLAIM_KEY_ACCOUNT_ENABLED, user.isEnabled());
            claims.put(CLAIM_KEY_ACCOUNT_NON_LOCKED, user.isAccountNonLocked());
            claims.put(CLAIM_KEY_ACCOUNT_NON_EXPIRED, user.isAccountNonExpired());
            if (!CollectionUtils.isEmpty(user.getAuthorities())) {
                claims.put(CLAIM_KEY_AUTHORITIES , JSON.toJSON(getAuthorities(user.getAuthorities())));
            }
            return claims;
        }
    
        /**
         * 获取角色权限
         * @param authorities
         * @return
         */
        public List<String> getAuthorities(Collection<? extends GrantedAuthority> authorities){
            List<String> list = new ArrayList<>();
            for (GrantedAuthority ga : authorities) {
                list.add(ga.getAuthority());
            }
            return list;
        }
    
    }
    
    

    6.5 Spring Security引入

    自定义UserDetails:

    package com.example.springboot.jwt.core.jwt.userdetails;
    
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.time.Instant;
    import java.util.Collection;
    import java.util.List;
    
    /**
     * <pre>
     *  JWTUserDetails
     * </pre>
     *
     * <pre>
     * @author mazq
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/07/06 14:45  修改内容:
     * </pre>
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class JWTUserDetails implements UserDetails {
    
        /**
         * 用户ID
         */
        private Long userId;
        /**
         * 用户密码
         */
        private String password;
        /**
         * 用户名
         */
        private String username;
        /**
         * 用户角色权限
         */
        private Collection<? extends GrantedAuthority> authorities;
        /**
         * 账号是否过期
         */
        private  Boolean isAccountNonExpired = false;
        /**
         * 账户是否锁定
         */
        private  Boolean isAccountNonLocked = false;
        /**
         * 密码是否过期
         */
        private  Boolean isCredentialsNonExpired = false;
        /**
         * 账号是否激活
         */
        private  Boolean isEnabled = true;
        /**
         * 上次密码重置时间
         */
        private  Instant lastPasswordResetDate;
    
        public JWTUserDetails(Long id, String username, String password, List<GrantedAuthority> mapToGrantedAuthorities) {
            this.userId = id;
            this.username = username;
            this.password = password;
            this.authorities = mapToGrantedAuthorities;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @JsonIgnore
        @Override
        public boolean isAccountNonExpired() {
            return isAccountNonExpired;
        }
    
        @JsonIgnore
        @Override
        public boolean isAccountNonLocked() {
            return isAccountNonLocked;
        }
    
        @JsonIgnore
        @Override
        public boolean isCredentialsNonExpired() {
            return isCredentialsNonExpired;
        }
    
        @JsonIgnore
        @Override
        public boolean isEnabled() {
            return isEnabled;
        }
    
    
    }
    
    

    UserDetailsServiceImpl.java业务接口

    package com.example.springboot.jwt.service;
    
    import com.example.springboot.jwt.core.jwt.userdetails.JWTUserDetails;
    import com.example.springboot.jwt.mapper.UserMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    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.Service;
    
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * <pre>
     *  UserDetailsServiceImpl
     * </pre>
     *
     * <pre>
     * @author mazq
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/07/06 18:10  修改内容:
     * </pre>
     */
    @Service("jwtUserService")
    @Slf4j
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        @Qualifier("userMapper")
        UserMapper userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            JWTUserDetails user = userRepository.findByUsername(username);
            if(user == null){
                log.info("登录用户[{}]没注册!",username);
                throw new UsernameNotFoundException("登录用户["+username + "]没注册!");
            }
            return new JWTUserDetails(1L,user.getUsername(), user.getPassword(), getAuthority());
        }
    
        private List<GrantedAuthority> getAuthority() {
            return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
        }
    }
    
    

    自定义AuthenticationEntryPoint进行统一异常处理:

    package com.example.springboot.jwt.web.handler;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.Serializable;
    
    /**
     * <pre>
     *  JWTAuthenticationEntryPoint
     * </pre>
     *
     * <pre>
     * @author mazq
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/07/09 14:46  修改内容:
     * </pre>
     */
    @Component
    public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
    
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            // 出错时候
            httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
        }
    }
    
    

    6.6 JWT授权过滤器

    package com.example.springboot.jwt.web.filter;
    
    import com.example.springboot.jwt.configuration.JWTProperties;
    import com.example.springboot.jwt.core.jwt.userdetails.JWTUserDetails;
    import com.example.springboot.jwt.core.jwt.util.JWTTokenUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.util.PathMatcher;
    import org.springframework.util.StringUtils;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    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.Arrays;
    import java.util.List;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    
    /**
     * <pre>
     *  JWTAuthenticationTokenFilter
     * </pre>
     *
     * <pre>
     * @author mazq
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/07/06 16:04  修改内容:
     * </pre>
     */
    @Slf4j
    public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
    
        private static final ConcurrentMap<String,Boolean> URI_CACHE_MAP = new ConcurrentHashMap<String,Boolean>();
        private final List<String> permitAllUris;
        private final List<String> authenticateUris;
    
        @Autowired
        JWTProperties jwtProperties;
        @Autowired
        JWTTokenUtil jwtTokenUtil;
        @Autowired
        @Qualifier("jwtUserService")
        UserDetailsService userDetailsService;
    
        public JWTAuthenticationTokenFilter(JWTProperties jwtProperties) {
            this.permitAllUris = Arrays.asList(jwtProperties.getPermitAll().split(","));
            this.authenticateUris = Arrays.asList(jwtProperties.getAuthenticateUri().split(","));
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                        FilterChain filterChain) throws ServletException, IOException {
            if (!isAllowUri(httpServletRequest)) {
                final String _authHeader = httpServletRequest.getHeader(jwtProperties.getTokenKey());
                log.info("Authorization:[{}]",_authHeader);
                if (StringUtils.isEmpty(_authHeader) || ! _authHeader.startsWith(jwtProperties.getTokenPrefix())) {
                    throw new RuntimeException("Unable to get JWT Token");
                }
                final String token = _authHeader.substring(7);
                log.info("acceptToken:[{}]",token);
                if (!jwtTokenUtil.validateToken(token)) {
                    throw new RuntimeException("Invalid token");
                }
                if (jwtTokenUtil.validateToken(token)) {
                    String username = jwtTokenUtil.getUsernameFromClaims(token);
                    JWTUserDetails userDetails = (JWTUserDetails)userDetailsService.loadUserByUsername(username);
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    usernamePasswordAuthenticationToken
                            .setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    
        private Boolean isAllowUri(HttpServletRequest request) {
            String uri = request.getServletPath();
            if (URI_CACHE_MAP.containsKey(uri)) {
                // 缓存有数据,直接从缓存读取
                return URI_CACHE_MAP.get(uri);
            }
            boolean flag = checkRequestUri(uri);
            // 数据丢到缓存里
            URI_CACHE_MAP.putIfAbsent(uri, flag);
            return flag;
        }
    
        private Boolean checkRequestUri(String requestUri) {
            boolean filter = true;
            final PathMatcher pathMatcher = new AntPathMatcher();
            for (String permitUri : permitAllUris) {
                if (pathMatcher.match(permitUri, requestUri)) {
                    // permit all的链接直接放过
                    filter = true;
                }
            }
            for (String authUri : authenticateUris) {
                if (pathMatcher.match(authUri, requestUri)) {
                    filter = false;
                }
            }
            return filter;
        }
    }
    
    

    WebMvcConfigurer类注册过滤器:

    package com.example.springboot.jwt.configuration;
    
    import com.example.springboot.jwt.web.filter.JWTAuthenticationTokenFilter;
    import com.example.springboot.jwt.web.handler.SecurityHandlerInterceptor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * <pre>
     *  MyWebMvcConfigurer
     * </pre>
     *
     * <pre>
     * @author mazq
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/07/07 13:52  修改内容:
     * </pre>
     */
    @Configuration
    
    public class MyWebMvcConfigurer implements WebMvcConfigurer {
    
        @Autowired
        private JWTProperties jwtProperties;
       
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new SecurityHandlerInterceptor())
                    .addPathPatterns("/**");
        }
    
        @Bean
        public JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
            return new JWTAuthenticationTokenFilter(jwtProperties);
        }
    
        @Bean
        public FilterRegistrationBean jwtFilter() {
            FilterRegistrationBean registrationBean = new FilterRegistrationBean();
            registrationBean.setFilter(jwtAuthenticationTokenFilter());
            return registrationBean;
        }
    
    
       
    
    }
    
    

    6.7 Spring Security配置类

    package com.example.springboot.jwt.configuration;
    
    
    import com.example.springboot.jwt.core.encode.CustomPasswordEncoder;
    import com.example.springboot.jwt.web.filter.JWTAuthenticationTokenFilter;
    import com.example.springboot.jwt.web.handler.JWTAuthenticationEntryPoint;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    /**
     * <pre>
     *  SecurityConfiguration
     * </pre>
     *
     * <pre>
     * @author mazq
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/04/30 15:58  修改内容:
     * </pre>
     */
    @Configuration
    @EnableWebSecurity
    @Order(1)
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("jwtUserService")
        private UserDetailsService userDetailsService;
        @Autowired
        private JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;
        @Autowired
        private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService)
                    .passwordEncoder(new CustomPasswordEncoder());
            auth.parentAuthenticationManager(authenticationManagerBean());
    
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            //解决静态资源被拦截的问题
            web.ignoring().antMatchers("/asserts/**");
            web.ignoring().antMatchers("/favicon.ico");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http   // 配置登录页并允许访问
                    .formLogin().loginPage("/login").permitAll()
                    // 登录成功被调用
                    //.successHandler(new MyAuthenticationSuccessHandler())
                    // 配置登出页面
                    .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                    .and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**","/authenticate/**").permitAll()
                    // 其余所有请求全部需要鉴权认证
                    .anyRequest().authenticated()
                    // 自定义authenticationEntryPoint
                    .and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint )
                    // 不使用Session
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    // 关闭跨域保护;
                    .and().csrf().disable();
            // JWT 过滤器
            http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    
        }
    
    
    
        @Bean
        public PasswordEncoder bcryptPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    
    }
    
    

    6.8 自定义登录页面

    <!DOCTYPE html>
    <html lang="zh" xmlns:th="http://www.thymeleaf.org">
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
            <meta name="description" content="" />
            <meta name="author" content="" />
            <title>Signin Template for Bootstrap</title>
            <!-- Bootstrap core CSS -->
            <link href="../static/asserts/css/bootstrap.min.css" th:href="@{asserts/css/bootstrap.min.css}" rel="stylesheet" />
            <!-- Custom styles for this template -->
            <link href="../static/asserts/css/signin.css" th:href="@{asserts/css/signin.css}" rel="stylesheet"/>
        </head>
    
        <body class="text-center">
            <form class="form-signin" th:action="@{/authenticate}" method="post">
                <img class="mb-4" th:src="@{asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72" />
                <h1 class="h3 mb-3 font-weight-normal" th:text="#{messages.tip}">Oauth2.0 Login</h1>
                <label class="sr-only" th:text="#{messages.username}">Username</label>
                <input type="text" class="form-control" name="username" id="username" th:placeholder="#{messages.username}" required="" autofocus="" value="nicky" />
                <label class="sr-only" th:text="#{messages.password} ">Password</label>
                <input type="password" class="form-control" name="password" id="password" th:placeholder="#{messages.password}" required="" value="123" />
                <div class="checkbox mb-3">
                    <label>
              <input type="checkbox" value="remember-me"  /> remember me
            </label>
                </div>
                <button class="btn btn-lg btn-primary btn-block" id="btnSave" type="submit" th:text="#{messages.loginBtnName}">Sign in</button>
                <p class="mt-5 mb-3 text-muted">© 2019</p>
                <a class="btn btn-sm" th:href="@{/login(lang='zh_CN')} ">中文</a>
                <a class="btn btn-sm" th:href="@{/login(lang='en_US')} ">English</a>
            </form>
            <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script>
            <script>
                $(function() {
                    $("#btnSave").click(function () {
                        var username=$("#username").val();
                        var password=$("#password").val();
                        $.ajax({
                            cache: false,
                            type: "POST",
                            url: "/authenticate",
                            contentType:"application/x-www-form-urlencoded; charset=UTF-8",
                            data:{"username":username ,"password" : password},
                            dataType: "json",
                            async: false,
                            error: function (request) {
                                console.log("Connection error");
                            },
                            success: function (data) {
                                //save token
                                localStorage.setItem("token",data);
                            }
                        });
                    });
                });
            </script>
    
        </body>
    
    </html>
    

    LoginController.java:

    
    
        @GetMapping(value = {"/login"})
        public ModelAndView toLogin(){
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.setViewName("login");
            return modelAndView;
        }
    
        @PostMapping(value = "/authenticate")
        @ResponseBody
        public ResponseEntity<?> authenticate( UserDto userDto, HttpServletRequest request,
                                               HttpServletResponse response) throws Exception {
            // ... 省略用户登录校验代码
            UserDetails userDetails = userDetailsService.loadUserByUsername(userDto.getUsername());
            String token = jwtTokenUtil.generateToken(userDetails);
            response.setHeader(jwtProperties.getTokenKey(),jwtProperties.getTokenPrefix()+token);
            return ResponseEntity.ok(token);
        }
        
    
    在这里插入图片描述

    输入账号密码,校验通过,返回jwt的令牌token

    eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX25hbWUiOiJuaWNreSIsInNjb3BlIjpbIlJPTEVfQURNSU4iXSwibm9uX2V4cGlyZWQiOmZhbHNlLCJleHAiOjE1OTQyODgyMzksImlhdCI6MTU5NDI4NDYzOCwiZW5hYmxlZCI6dHJ1ZSwibm9uX2xvY2tlZCI6ZmFsc2V9.bxGCCBSQE5cgVSl9Lve-vyDtITw1gL5i2-O-B5uEgno
    

    测试令牌,官方测试链接:https://jwt.io/#debugger-io

    在这里插入图片描述
    base64:
    在这里插入图片描述
    package com.example.springboot.jwt.web.controller;
    
    import com.example.springboot.jwt.configuration.JWTProperties;
    import com.example.springboot.jwt.core.jwt.util.JWTTokenUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * <pre>
     *  UserController
     * </pre>
     *
     * <pre>
     * @author mazq
     * 修改记录
     *    修改后版本:     修改人:  修改日期: 2020/07/07 14:14  修改内容:
     * </pre>
     */
    @RestController
    @RequestMapping(value = "api/user")
    public class UserController {
    
        @Autowired
        JWTProperties jwtProperties;
        @Autowired
        JWTTokenUtil jwtTokenUtil;
    
        @GetMapping("/auth-info")
        public ResponseEntity authInfo(HttpServletRequest request) {
            String authHeader = request.getHeader(jwtProperties.getTokenKey());
            String token = authHeader.substring(7);
            return ResponseEntity.ok(jwtTokenUtil.getUsernameFromClaims(token));
        }
    }
    
    

    复制生成的jwt令牌,设置Request Header

    在这里插入图片描述

    代码例子下载:下载

    相关文章

      网友评论

        本文标题:SpringBoot系列之前后端接口安全技术JWT

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