⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

摘要: 原创出处 dpb-bobokaoya-sm.blog.csdn.net/article/details/103409430 「波波烤鸭」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

通过前面几天文章我们详细的介绍了SpringSecurity的使用,本文我们来看下,结合JWT来实现单点登录操作。

一、什么是单点登陆

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

二、简单的运行机制

单点登录的机制其实是比较简单的,用一个现实中的例子做比较。某公园内部有许多独立的景点,游客可以在各个景点门口单独买票。对于需要游玩所有的景点的游客,这种买票方式很不方便,需要在每个景点门口排队买票,钱包拿 进拿出的,容易丢失,很不安全。于是绝大多数游客选择在大门口买一张通票(也叫套票),就可以玩遍所有的景点而不需要重新再买票。他们只需要在每个景点门 口出示一下刚才买的套票就能够被允许进入每个独立的景点。 单点登录的机制也一样,如下图所示,

用户认证:这一环节主要是用户向认证服务器发起认证请求,认证服务器给用户返回一个成功的令牌token,主要在认证服务器中完成,即图中的认证系统,注意认证系统只能有一个。 身份校验:这一环节是用户携带token去访问其他服务器时,在其他服务器中要对token的真伪进行检验,主要在资源服务器中完成,即图中的应用系统2 3

三、JWT介绍

概念说明

从分布式认证流程中,我们不难发现,这中间起最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,这里我们选择使用JWT来实现token的生成和校验。 JWT,全称JSON Web Token,官网地址https://jwt.io,是一款出色的分布式身份校验方案。可以生成token,也可以解析检验token。

JWT生成的token由三部分组成:

头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。 载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露! 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。

JWT生成token的安全性分析

从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的base64编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的上面了!试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术公开了?大家可以用这个盐来解析token,就能用来伪造token。这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!

非对称加密RSA介绍

基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端 私钥加密,持有私钥或公钥才可以解密 公钥加密,持有私钥才可解密 优点:安全,难以破解 缺点:算法比较耗时,为了安全,可以接受 历史:三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA。

四、SpringSecurity整合JWT

1.认证思路分析

SpringSecurity主要是通过过滤器来实现功能的!我们要找到SpringSecurity实现认证和校验身份的过滤器!

回顾集中式认证流程

用户认证: 使用UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法实现认证功能,该过滤器父类中successfulAuthentication方法实现认证成功后的操作。 身份校验: 使用BasicAuthenticationFilter过滤器中doFilterInternal方法验证是否登录,以决定能否进入后续过滤器。

分析分布式认证流程

用户认证: 由于分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。 另外,默认successfulAuthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。 身份校验: 原来BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于后续的授权功能可以正常使用。

2.具体实现

为了演示单点登录的效果,我们设计如下项目结构

2.1父工程创建

因为本案例需要创建多个系统,所以我们使用maven聚合工程来实现,首先创建一个父工程,导入springboot的父依赖即可

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>

2.2公共工程创建

然后创建一个common工程,其他工程依赖此系统 导入JWT相关的依赖

<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<!--jackson包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.9</version>
</dependency>
<!--日志包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>

创建相关的工具类

Payload

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 10:28
*/
@Data
public class Payload <T>{
private String id;
private T userInfo;
private Date expiration;
}

JsonUtils

package com.dpb.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
* @author: 波波烤鸭
**/
public class JsonUtils {

public static final ObjectMapper mapper = new ObjectMapper();

private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);

public static String toString(Object obj) {
if (obj == null) {
return null;
}
if (obj.getClass() == String.class) {
return (String) obj;
}
try {
return mapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
logger.error("json序列化出错:" + obj, e);
return null;
}
}

public static <T> T toBean(String json, Class<T> tClass) {
try {
return mapper.readValue(json, tClass);
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}

public static <E> List<E> toList(String json, Class<E> eClass) {
try {
return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, eClass));
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}

public static <K, V> Map<K, V> toMap(String json, Class<K> kClass, Class<V> vClass) {
try {
return mapper.readValue(json, mapper.getTypeFactory().constructMapType(Map.class, kClass, vClass));
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}

public static <T> T nativeRead(String json, TypeReference<T> type) {
try {
return mapper.readValue(json, type);
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}
}

JwtUtils

package com.dpb.utils;

import com.dpb.domain.Payload;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;

import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import java.util.UUID;

/**
* @author: 波波烤鸭
* 生成token以及校验token相关方法
*/
public class JwtUtils {

private static final String JWT_PAYLOAD_USER_KEY = "user";

/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位分钟
* @return JWT
*/
public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusMinutes(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}

/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位秒
* @return JWT
*/
public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}

/**
* 公钥解析token
*
* @param token 用户请求中的token
* @param publicKey 公钥
* @return Jws<Claims>
*/
private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}

private static String createJTI() {
return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
}

/**
* 获取token中的用户信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
claims.setExpiration(body.getExpiration());
return claims;
}

/**
* 获取token中的载荷信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setExpiration(body.getExpiration());
return claims;
}
}

RsaUtils

package com.dpb.utils;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
* @author 波波烤鸭
*/
public class RsaUtils {

private static final int DEFAULT_KEY_SIZE = 2048;
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}

/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}

/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
private static PublicKey getPublicKey(byte[] bytes) throws Exception {
bytes = Base64.getDecoder().decode(bytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}

/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
bytes = Base64.getDecoder().decode(bytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}

/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
writeFile(privateKeyFilename, privateKeyBytes);
}

private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}

private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}

在通用子模块中编写测试类生成rsa公钥和私钥

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 11:08
*/
public class JwtTest {
private String privateKey = "c:/tools/auth_key/id_key_rsa";

private String publicKey = "c:/tools/auth_key/id_key_rsa.pub";

@Test
public void test1() throws Exception{
RsaUtils.generateKey(publicKey,privateKey,"dpb",1024);
}

}

2.3认证系统创建

接下来我们创建我们的认证服务。

导入相关的依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<artifactId>security-jwt-common</artifactId>
<groupId>com.dpb</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

创建配置文件

spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/srm
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
type-aliases-package: com.dpb.domain
mapper-locations: classpath:mapper/*.xml
logging:
level:
com.dpb: debug
rsa:
key:
pubKeyFile: c:\tools\auth_key\id_key_rsa.pub
priKeyFile: c:\tools\auth_key\id_key_rsa

提供公钥私钥的配置类

package com.dpb.config;

import com.dpb.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 11:25
*/
@Data
@ConfigurationProperties(prefix = "rsa.key")
public class RsaKeyProperties {

private String pubKeyFile;
private String priKeyFile;

private PublicKey publicKey;
private PrivateKey privateKey;

/**
* 系统启动的时候触发
* @throws Exception
*/
@PostConstruct
public void createRsaKey() throws Exception {
publicKey = RsaUtils.getPublicKey(pubKeyFile);
privateKey = RsaUtils.getPrivateKey(priKeyFile);
}

}

创建启动类

/**
* @program: springboot-54-security-jwt-demo
* @description: 启动类
* @author: 波波烤鸭
* @create: 2019-12-03 11:23
*/
@SpringBootApplication
@MapperScan("com.dpb.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class App {

public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}

完成数据认证的逻辑

pojo

package com.dpb.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 15:21
*/
@Data
public class RolePojo implements GrantedAuthority {

private Integer id;
private String roleName;
private String roleDesc;

@JsonIgnore
@Override
public String getAuthority() {
return roleName;
}
}

package com.dpb.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 11:33
*/
@Data
public class UserPojo implements UserDetails {

private Integer id;

private String username;

private String password;

private Integer status;

private List<RolePojo> roles;

@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> auth = new ArrayList<>();
auth.add(new SimpleGrantedAuthority("ADMIN"));
return auth;
}

@Override
public String getPassword() {
return this.password;
}

@Override
public String getUsername() {
return this.username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}

Mapper接口

public interface UserMapper {
public UserPojo queryByUserName(@Param("userName") String userName);
}

Mapper映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dpb.mapper.UserMapper">
<select id="queryByUserName" resultType="UserPojo">
select * from t_user where username = #{userName}
</select>
</mapper>

Service

public interface UserService extends UserDetailsService {

}

@Service
@Transactional
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper mapper;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserPojo user = mapper.queryByUserName(s);

return user;
}
}

自定义认证过滤器

package com.dpb.filter;

import com.dpb.config.RsaKeyProperties;
import com.dpb.domain.RolePojo;
import com.dpb.domain.UserPojo;
import com.dpb.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.bytebuddy.agent.builder.AgentBuilder;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 11:57
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;

public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class);

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
return authenticationManager.authenticate(authRequest);
}catch (Exception e){
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "用户名或密码错误!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}catch (Exception outEx){
outEx.printStackTrace();
}
throw new RuntimeException(e);
}
}

public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserPojo user = new UserPojo();
user.setUsername(authResult.getName());
user.setRoles((List<RolePojo>)authResult.getAuthorities());
String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60);
response.addHeader("Authorization", "Bearer "+token);
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_OK);
resultMap.put("msg", "认证通过!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}catch (Exception outEx){
outEx.printStackTrace();
}
}
}

自定义校验token的过滤器

package com.dpb.filter;

import com.dpb.config.RsaKeyProperties;
import com.dpb.domain.Payload;
import com.dpb.domain.UserPojo;
import com.dpb.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 12:39
*/
public class TokenVerifyFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;

public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
super(authenticationManager);
this.prop = prop;
}

public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
//如果携带错误的token,则给用户提示请登录!
chain.doFilter(request, response);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
resultMap.put("msg", "请登录!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} else {
//如果携带了正确格式的token要先得到token
String token = header.replace("Bearer ", "");
//验证tken是否正确
Payload<UserPojo> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), UserPojo.class);
UserPojo user = payload.getUserInfo();
if(user!=null){
UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
}

}

###编写SpringSecurity的配置类

package com.dpb.config;

import com.dpb.filter.TokenLoginFilter;
import com.dpb.filter.TokenVerifyFilter;
import com.dpb.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 12:41
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserService userService;

@Autowired
private RsaKeyProperties prop;

@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

//指定认证对象的来源
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
//SpringSecurity配置信息
public void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/user/query").hasAnyRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.addFilter(new TokenLoginFilter(super.authenticationManager(), prop))
.addFilter(new TokenVerifyFilter(super.authenticationManager(), prop))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

启动服务测试

启动服务

通过Postman来访问测试

根据token信息我们访问其他资源

2.4资源系统创建

说明 资源服务可以有很多个,这里只拿产品服务为例,记住,资源服务中只能通过公钥验证认证。不能签发token!创建产品服务并导入jar包根据实际业务导包即可,咱们就暂时和认证服务一样了。

接下来我们再创建一个资源服务

导入相关的依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<artifactId>security-jwt-common</artifactId>
<groupId>com.dpb</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

编写产品服务配置文件

切记这里只能有公钥地址!

server:
port: 9002
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/srm
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
type-aliases-package: com.dpb.domain
mapper-locations: classpath:mapper/*.xml
logging:
level:
com.dpb: debug
rsa:
key:
pubKeyFile: c:\tools\auth_key\id_key_rsa.pub

编写读取公钥的配置类

package com.dpb.config;

import com.dpb.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import javax.annotation.PostConstruct;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 11:25
*/
@Data
@ConfigurationProperties(prefix = "rsa.key")
public class RsaKeyProperties {

private String pubKeyFile;

private PublicKey publicKey;

/**
* 系统启动的时候触发
* @throws Exception
*/
@PostConstruct
public void createRsaKey() throws Exception {
publicKey = RsaUtils.getPublicKey(pubKeyFile);
}

}

编写启动类

package com.dpb;

import com.dpb.config.RsaKeyProperties;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 17:23
*/
@SpringBootApplication
@MapperScan("com.dpb.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class App {

public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}

复制认证服务中,用户对象,角色对象和校验认证的接口

复制认证服务中的相关内容即可

复制认证服务中SpringSecurity配置类做修改

package com.dpb.config;

import com.dpb.filter.TokenVerifyFilter;
import com.dpb.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 12:41
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserService userService;

@Autowired
private RsaKeyProperties prop;

@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

//指定认证对象的来源
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
//SpringSecurity配置信息
public void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
//.antMatchers("/user/query").hasAnyRole("USER")
.anyRequest()
.authenticated()
.and()
.addFilter(new TokenVerifyFilter(super.authenticationManager(), prop))
// 禁用掉session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

去掉“增加自定义认证过滤器”即可!

编写产品处理器

package com.dpb.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @program: springboot-54-security-jwt-demo
* @description:
* @author: 波波烤鸭
* @create: 2019-12-03 11:55
*/
@RestController
@RequestMapping("/user")
public class UserController {

@RequestMapping("/query")
public String query(){
return "success";
}

@RequestMapping("/update")
public String update(){
return "update";
}
}

测试

搞定~

文章目录
  1. 1. 一、什么是单点登陆
  2. 2. 二、简单的运行机制
  3. 3. 三、JWT介绍
    1. 3.1. 概念说明
    2. 3.2. JWT生成的token由三部分组成:
    3. 3.3. JWT生成token的安全性分析
    4. 3.4. 非对称加密RSA介绍
  4. 4. 四、SpringSecurity整合JWT
    1. 4.1. 1.认证思路分析
      1. 4.1.1. 回顾集中式认证流程
      2. 4.1.2. 分析分布式认证流程
    2. 4.2. 2.具体实现
      1. 4.2.1. 2.1父工程创建
      2. 4.2.2. 2.2公共工程创建
    3. 4.3. 2.3认证系统创建
      1. 4.3.1. 导入相关的依赖
      2. 4.3.2. 创建配置文件
      3. 4.3.3. 提供公钥私钥的配置类
      4. 4.3.4. 创建启动类
      5. 4.3.5. 完成数据认证的逻辑
      6. 4.3.6. 自定义认证过滤器
      7. 4.3.7. 自定义校验token的过滤器
      8. 4.3.8. 启动服务测试
    4. 4.4. 2.4资源系统创建
      1. 4.4.1. 导入相关的依赖
      2. 4.4.2. 编写产品服务配置文件
      3. 4.4.3. 编写读取公钥的配置类
      4. 4.4.4. 编写启动类
      5. 4.4.5. 复制认证服务中,用户对象,角色对象和校验认证的接口
      6. 4.4.6. 复制认证服务中SpringSecurity配置类做修改
      7. 4.4.7. 编写产品处理器
      8. 4.4.8. 测试