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

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

    • 1. 什么是JWT?
    • 2. JWT令牌结构怎么样?
      • 2.1 标头(Header)
      • 2.2 有效载荷(Playload)
      • 2.3 签名(Signature)
    • 3. JWT原理简单介绍
    • 4. JWT的应用场景
    • 5. 与Cookie-Session对比
    • 6. Java的JJWT实现JWT
      • 6.1 什么是JJWT?
      • 6.2 实验环境准备
      • 6.3 jwt配置属性读取
      • 6.4 JWT Token工具类
      • 6.5 Spring Security引入
      • 6.6 JWT授权过滤器
      • 6.7 Spring Security配置类
      • 6.8 自定义登录页面

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格式如图:
SpringBoot系列之前后端接口安全技术JWT_第1张图片

2. JWT令牌结构怎么样?

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

  • 标头(Header)
  • 有效载荷(Playload)
  • 签名(Signature)
    因此,JWT通常如下所示。
    xxxxx.yyyyy.zzzzz
    SpringBoot系列之前后端接口安全技术JWT_第2张图片

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:
SpringBoot系列之前后端接口安全技术JWT_第3张图片

JWT官网提供的在线调试工具:
https://jwt.io/#debugger-io
SpringBoot系列之前后端接口安全技术JWT_第4张图片
开源中国提供的base64在线加解密:
https://tool.oschina.net/encrypt?type=3
SpringBoot系列之前后端接口安全技术JWT_第5张图片

3. JWT原理简单介绍

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

SpringBoot系列之前后端接口安全技术JWT_第6张图片

  • 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.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>${jjwt.version}version>
        dependency>
        <dependency>
            <groupId>com.auth0groupId>
            <artifactId>java-jwtartifactId>
            <version>${java.jwt.version}version>
        dependency>

pom.xml:



<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.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.2.1.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.example.springbootgroupId>
    <artifactId>springboot-jwtartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>springboot-jwtname>
    <description>Demo project for Spring Bootdescription>

    <properties>
        <java.version>1.8java.version>
        <jjwt.version>0.9.0jjwt.version>
        <java.jwt.version>3.4.0java.jwt.version>
        <mybatis.springboot.version>2.1.1mybatis.springboot.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>

        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>${jjwt.version}version>
        dependency>
        <dependency>
            <groupId>com.auth0groupId>
            <artifactId>java-jwtartifactId>
            <version>${java.jwt.version}version>
        dependency>

        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>${mybatis.springboot.version}version>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.27version>
            <scope>runtimescope>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.47version>
            <scope>compilescope>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

application.yml:

spring:
  datasource:
    url: jdbc:mysql://192.168.0.199:3306/jeeplatform?autoReconnect=true&useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=false
    username: root
    password: secret
    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

项目工程:
SpringBoot系列之前后端接口安全技术JWT_第7张图片

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;

/**
 * 
 *  JWT配置类
 * 
* *
 * @author nicky.ma
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/07/06 11:37  修改内容:
 * 
*/
@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;

/**
 * 
 *  YAML配置文件读取工厂类
 * 
*

*

 * @author nicky.ma
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2019/11/13 15:44  修改内容:
 * 
*/
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.*;


/**
 * 
 *   JWT工具类
 * 
* *
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/07/06 13:57  修改内容:
 * 
*/
@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;

/**
 * 
 *  JWTUserDetails
 * 
* *
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/07/06 14:45  修改内容:
 * 
*/
@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;

/**
 * 
 *  UserDetailsServiceImpl
 * 
* *
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/07/06 18:10  修改内容:
 * 
*/
@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;

/**
 * 
 *  JWTAuthenticationEntryPoint
 * 
* *
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/07/09 14:46  修改内容:
 * 
*/
@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;

/**
 * 
 *  JWTAuthenticationTokenFilter
 * 
* *
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/07/06 16:04  修改内容:
 * 
*/
@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;

/**
 * 
 *  MyWebMvcConfigurer
 * 
* *
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/07/07 13:52  修改内容:
 * 
*/
@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;

/**
 * 
 *  SecurityConfiguration
 * 
* *
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/04/30 15:58  修改内容:
 * 
*/
@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 自定义登录页面


<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 Bootstraptitle>
		
		<link href="../static/asserts/css/bootstrap.min.css" th:href="@{asserts/css/bootstrap.min.css}" rel="stylesheet" />
		
		<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 Loginh1>
			<label class="sr-only" th:text="#{messages.username}">Usernamelabel>
			<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} ">Passwordlabel>
			<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 inbutton>
			<p class="mt-5 mb-3 text-muted">© 2019p>
			<a class="btn btn-sm" th:href="@{/login(lang='zh_CN')} ">中文a>
			<a class="btn btn-sm" th:href="@{/login(lang='en_US')} ">Englisha>
		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);
    }
	

SpringBoot系列之前后端接口安全技术JWT_第8张图片
输入账号密码,校验通过,返回jwt的令牌token

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX25hbWUiOiJuaWNreSIsInNjb3BlIjpbIlJPTEVfQURNSU4iXSwibm9uX2V4cGlyZWQiOmZhbHNlLCJleHAiOjE1OTQyODgyMzksImlhdCI6MTU5NDI4NDYzOCwiZW5hYmxlZCI6dHJ1ZSwibm9uX2xvY2tlZCI6ZmFsc2V9.bxGCCBSQE5cgVSl9Lve-vyDtITw1gL5i2-O-B5uEgno

测试令牌,官方测试链接:https://jwt.io/#debugger-io
SpringBoot系列之前后端接口安全技术JWT_第9张图片
base64:
SpringBoot系列之前后端接口安全技术JWT_第10张图片

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;

/**
 * 
 *  UserController
 * 
* *
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/07/07 14:14  修改内容:
 * 
*/
@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_第11张图片

  • 附录:
    https://www.javainuse.com/spring/boot-jwt

代码例子下载:下载

你可能感兴趣的:(SpringBoot,Java框架,jwt,oauth,spring,springBoot,鉴权)