@

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. git clone 时注意点

    环境: 在公司访问外网需要设置代理,另外,在公司局域网内架设了一台 GIT 服务器. 在使用 git clone 时,不能设置成 git 使用代理: git config --global http. ...

  2. [转] C++中的namespace

    点击阅读原文 namespace中文意思是命名空间或者叫名字空间,传统的C++只有一个全局的namespace,但是由于现在的程序的规模越来越大,程序的分工越来越细,全局作用域变得越来越拥挤,每个人都 ...

  3. String Problem(模板)【最短路】

    String Problem 题目链接(点击) Boy Valera likes strings. And even more he likes them, when they are identic ...

  4. vsftpd服务器配置与使用

    1.ftp简介 网络文件的共享主流的主要有三种,分别为ftp.nfs.samba ftp用于internet上的控制文件的双向传输 上传和下载的操作 下载 上传 将主机中的内容拷贝到计算机上 将文件从 ...

  5. if test表达式逻辑判断不能用&&

    用&&会报错 用and 例如: <if test="age!=null and name!=null">

  6. Ubuntu U盘启动出现“Failed to load ldlinux.c32”解决

    最后用ultraISO软碟通,刻录映像时写入方式选择”RAW”,成功解决!!!

  7. C#中的闭包和意想不到的坑

    虽然闭包主要是函数式编程的玩意儿,而C#的最主要特征是面向对象,但是利用委托或lambda表达式,C#也可以写出具有函数式编程风味的代码.同样的,使用委托或者lambda表达式,也可以在C#中使用闭包 ...

  8. Dubbo面试专题

    Dubbo面试专题 1. 什么是dubbo Dubbo是阿里巴巴SOA服务化治理方案的核心框架,是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案. 2.  ...

  9. 15.DRF-分页

    Django rest framework(6)----分页 第一种分页 PageNumberPagination 基本使用 (1)urls.py urlpatterns = [ re_path('( ...

  10. bugku社工writeup

    最近bugku的web和杂项刷了多半,突然心血来潮想试试社工题,bugku的社工题比较基础,而且题量不多,和大家分享一下writeup. 1.密码 根据提示,多猜几次密码就对了,然后得到flag. 2 ...