@

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的更多相关文章

  1. SpringBoot系列之从入门到精通系列教程

    对应SpringBoot系列博客专栏,例子代码,本博客不定时更新 Spring框架:作为JavaEE框架领域的一款重要的开源框架,在企业应用开发中有着很重要的作用,同时Spring框架及其子框架很多, ...

  2. Springboot系列(七) 集成接口文档swagger,使用,测试

    Springboot 配置接口文档swagger 往期推荐 SpringBoot系列(一)idea新建Springboot项目 SpringBoot系列(二)入门知识 springBoot系列(三)配 ...

  3. SpringBoot系列——Spring-Data-JPA(究极进化版) 自动生成单表基础增、删、改、查接口

    前言 我们在之前的实现了springboot与data-jpa的增.删.改.查简单使用(请戳:SpringBoot系列——Spring-Data-JPA),并实现了升级版(请戳:SpringBoot系 ...

  4. SpringBoot写后端接口,看这一篇就够了!

    摘要:本文演示如何构建起一个优秀的后端接口体系,体系构建好了自然就有了规范,同时再构建新的后端接口也会十分轻松. 一个后端接口大致分为四个部分组成:接口地址(url).接口请求方式(get.post等 ...

  5. 【项目实践】SpringBoot三招组合拳,手把手教你打出优雅的后端接口

    以项目驱动学习,以实践检验真知 前言 一个后端接口大致分为四个部分组成:接口地址(url).接口请求方式(get.post等).请求数据(request).响应数据(response).如何构建这几个 ...

  6. SpringCloud微服务之跨服务调用后端接口

    SpringCloud微服务系列博客: SpringCloud微服务之快速搭建EurekaServer:https://blog.csdn.net/egg1996911/article/details ...

  7. SpringBoot系列-整合Mybatis(注解方式)

    目录 一.常用注解说明 二.实战 三.测试 四.注意事项 上一篇文章<SpringBoot系列-整合Mybatis(XML配置方式)>介绍了XML配置方式整合的过程,本文介绍下Spring ...

  8. SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver

    关于Web应用的全局异常处理,上一篇介绍了ControllerAdvice结合@ExceptionHandler的方式来实现web应用的全局异常管理: 本篇博文则带来另外一种并不常见的使用方式,通过实 ...

  9. SpringBoot系列教程web篇之重定向

    原文地址: SpringBoot系列教程web篇之重定向 前面介绍了spring web篇数据返回的几种常用姿势,当我们在相应一个http请求时,除了直接返回数据之外,还有另一种常见的case -&g ...

随机推荐

  1. Asp.Net Core入门之自定义服务注册

    谈到服务注册,首先我们先了解一下服务注册时使用的三种方式,也代表了不同的服务生命周期: AddTransient AddScoped AddSingleton AddSingleton生命周期最长,其 ...

  2. tp5的 LayUI分页样式实现

    1.先配置你的分页参数: //分页配置 'paginate'      => [ 'type'      => 'Layui', 'var_page'  => 'page', 'li ...

  3. Java 多线程基础(五)线程同步

    Java 多线程基础(五)线程同步 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题. 要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制 ...

  4. Springboot搭建Eureka并设置Eureka登录账号密码

    Springboot搭建Eureka并设置Eureka登录账号密码 一.创建一个springboot项目 1.可以使用Spring Initializr,用浏览器打开http://start.spri ...

  5. 「JOISC 2020 Day4」首都城市

    题目   点这里看题目. 分析   做法比较容易看出来.我们对于每个城市,找出那些 " 如果这个城市在首都内,则必须在首都内的其它城市 " ,也就是为了让这个城市的小镇连通而必须选 ...

  6. (三)MySQL基础查询(起别名、去重)

    资料下载请前往:链接 补充内容: 1.数据库基本结构: 2.在sqlyog中将myemployees库导入数据库的方法: 右键root@localhost ->选择 执行SQL脚本->选定 ...

  7. Asp.Net Mvc 控制器详解

    理解控制器 控制器的角色 (1)中转作用:控制器通过前面的学习大家应该知道它是一个承上启下的作用,根据用户输入,执行响应行为(动 作方法),同时在行为中调用模型的业务逻辑,返回给用户结果(视图). ( ...

  8. Java中泛型的继承

    最新在抽取公共方法的时候,遇到了需要使用泛型的情况,但是在搜索了一圈之后,发现大部分博客对于继承都说的不太清楚,所幸还有那么一两篇讲的清楚的,在这里自己标记下. 以我自己用到的代码举例,在父类中使用了 ...

  9. HTML新增的语义化标签及其作用

    在html5中,新增了几个语义化标签:<article>.<section>.<aside>.<hgroup>. <header>,< ...

  10. 手把手教你学Numpy,搞定数据处理——收官篇

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Numpy专题第6篇文章,我们一起来看看Numpy库当中剩余的部分. 数组的持久化 在我们做机器学习模型的研究或者是学习的时候,在完成 ...