什么是单点登录?
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
为什么要使用单点登录?
使用单点登录能够有效降低企业成本,提高员工工作效率,具体包括:
1、单点登录能够减少密码输入疲劳
2、单点登录通过减少登录次数来提高工作效率
3、单点登录为系统管理人员工作提供便利
4、单点登录能够提高系统安全性
5、单点登录简化了用户跟踪审计过程
对比单体应用的登录
单体应用登录的总体业务流程图:
基于JWT和RSA的Token机制
JWT简介
JSON Web Tokens 是JSON格式的加密字符串,用于加密验证信息,在前后端进行通信
分为三个部分
1、头部
2、负载
3、指纹
Java后台使用jjwt包来实现JWT的操作
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
JWT工具类
package com.blb.user_service.utils;
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;
/**
* JWT工具类
* @author dell
*/
public class JwtUtil {
public static final String JWT_KEY_USERNAME = "username";
public static final int EXPIRE_MINUTES = 120;
/**
* 私钥加密token
*/
public static String generateToken(String username, PrivateKey privateKey, int expireMinutes) {
return Jwts.builder()
.claim(JWT_KEY_USERNAME, username)
.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 从token解析用户
*
* @param token
* @param publicKey
* @return
* @throws Exception
*/
public static String getUsernameFromToken(String token, PublicKey publicKey){
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Claims body = claimsJws.getBody();
String username = (String) body.get(JWT_KEY_USERNAME);
return username;
}
}
RSA简介
RSA是一种非对称式的加密算法
对称式加密只有一个秘钥,加密和解密都通过该秘钥完成
非对称式加密有两个秘钥,公钥和私钥,加密和解密由公钥和私钥分开完成
RSA工具类
package com.blb.user_service.utils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* RSA工具类
* @author dell
*/
public class RsaUtil {
public static final String RSA_SECRET = "blbweb@#$%"; //秘钥
public static final String RSA_PATH = System.getProperty("user.dir")+"/rsa/"; //秘钥保存位置
public static final String RSA_PUB_KEY_PATH = RSA_PATH + "pubKey.rsa"; //公钥路径
public static final String RSA_PRI_KEY_PATH = RSA_PATH + "priKey.rsa"; //私钥路径
public static PublicKey publicKey; //公钥
public static PrivateKey privateKey; //私钥
/**
* 类加载后,生成公钥和私钥文件
*/
static {
try {
File rsa = new File(RSA_PATH);
if (!rsa.exists()) {
rsa.mkdirs();
}
File pubKey = new File(RSA_PUB_KEY_PATH);
File priKey = new File(RSA_PRI_KEY_PATH);
//判断公钥和私钥如果不存在就创建
if (!priKey.exists() || !pubKey.exists()) {
//创建公钥和私钥文件
RsaUtil.generateKey(RSA_PUB_KEY_PATH, RSA_PRI_KEY_PATH, RSA_SECRET);
}
//读取公钥和私钥内容
publicKey = RsaUtil.getPublicKey(RSA_PUB_KEY_PATH);
privateKey = RsaUtil.getPrivateKey(RSA_PRI_KEY_PATH);
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
/**
* 从文件中读取公钥
*
* @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
*/
public static PublicKey getPublicKey(byte[] bytes) throws Exception {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(1024, secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
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);
}
}
SpringSecurity登录处理的配置
响应状态枚举
package com.blb.user_service.utils;
/**
* 响应状态枚举
* @author dell
*/
public enum ResponseStatus {
/**
* 内置状态
*/
OK(200,"操作成功"),
INTERNAL_ERROR(500000,"系统错误"),
BUSINESS_ERROR(500001,"业务错误"),
LOGIN_ERROR(500002,"账号或密码错误"),
NO_DATA_ERROR(500003,"没有找到数据"),
PARAM_ERROR(500004,"参数格式错误"),
AUTH_ERROR(401,"没有权限,需要登录");
private Integer code;
private String message;
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
ResponseStatus(Integer status, String message) {
this.code = status;
this.message = message;
}
}
响应数据封装对象
package com.blb.user_service.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 响应数据封装对象
* @author dell
*/
@Data
public class ResponseResult<T> {
/**
* 状态信息
*/
private ResponseStatus status;
/**
* 数据
*/
private T data;
public ResponseResult(ResponseStatus status, T data) {
this.status = status;
this.data = data;
}
/**
* 返回成功对象
* @param data
* @return
*/
public static <T> ResponseResult<T> ok(T data){
return new ResponseResult<>(ResponseStatus.OK, data);
}
/**
* 返回错误对象
* @param status
* @return
*/
public static ResponseResult<String> error(ResponseStatus status){
return new ResponseResult<>(status,status.getMessage());
}
/**
* 返回错误对象
* @param status
* @return
*/
public static ResponseResult<String> error(ResponseStatus status, String msg){
return new ResponseResult<>(status,msg);
}
/**
* 向流中输出结果
* @param resp
* @param result
* @throws IOException
*/
public static void write(HttpServletResponse resp, ResponseResult result) throws IOException {
resp.setContentType("application/json;charset=UTF-8");
String msg = new ObjectMapper().writeValueAsString(result);
resp.getWriter().print(msg);
resp.getWriter().close();
}
}
SpringSecurity相关配置
package com.blb.user_service.config;
import com.blb.user_service.utils.ResponseResult;
import com.blb.user_service.utils.ResponseStatus;
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.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
/**
* SpringSecurity相关配置
*
* @author dell
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Qualifier("userDetailsServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
/**
* 密码加密器
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 设置自定义登录逻辑
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 页面资源的授权
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置放行URL
http.authorizeRequests()
.antMatchers("/swagger-ui.html", "/v2/**", "/swagger-resources/**",
"/webjars/springfox-swagger-ui/**", //放行swagger相关
"/js/**", "/css/**", //放行静态资源
"/login", "logout" //放行登录和登出
)
.permitAll()
.anyRequest()
.authenticated() //其它的请求需要登录
.and()
.formLogin()//登录配置
.successHandler(loginSuccessHandler) //登录成功处理
.failureHandler((req, resp, auth) -> { //配置登录失败的处理器
ResponseResult.write(resp, ResponseResult.error(ResponseStatus.LOGIN_ERROR));
})
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, auth) -> { //未进行登录请求的处理
ResponseResult.write(resp, ResponseResult.error(ResponseStatus.AUTH_ERROR));
})
.and()
.logout()//登出配置
.logoutSuccessHandler((req, resp, auth) -> { //配置登出的处理器
ResponseResult.write(resp, ResponseResult.ok("注销成功"));
})//退出登录处理
.clearAuthentication(true) //清除验证缓存
.and()
.csrf()
.disable() //关闭CSRF保护
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); //不使用session
}
}
封装用户信息实体类
package com.blb.user_service.entity.VO;
import com.blb.user_service.entity.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author dell
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserTokenVO {
private User user;
private String token;
}
登录验证过滤器
package com.blb.user_service.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.blb.user_service.entity.User;
import com.blb.user_service.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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;
/**
* 登录验证过滤器
* @author dell
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户名查询用户信息
User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getUsername, s));
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
//返回正确的用户信息
return new org.springframework.security.core.userdetails.User(s, user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
验证成功处理器
package com.blb.user_service.config;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.blb.user_service.entity.VO.UserTokenVO;
import com.blb.user_service.mapper.UserMapper;
import com.blb.user_service.utils.JwtUtil;
import com.blb.user_service.utils.ResponseResult;
import com.blb.user_service.utils.RsaUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author dell
*
* 登录成功处理器
*/
@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private UserMapper userMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
//获取用户信息
User user = (User) authentication.getPrincipal();
//将用户名生成JWT token
String token = JwtUtil.generateToken(user.getUsername(), RsaUtil.privateKey, JwtUtil.EXPIRE_MINUTES);
com.blb.user_service.entity.User user2 = userMapper.selectOne(new QueryWrapper<com.blb.user_service.entity.User>().lambda().eq(com.blb.user_service.entity.User::getUsername, user.getUsername()));
UserTokenVO userTokenVO = new UserTokenVO(user2, token);
//将token发送给前端
ResponseResult.write(httpServletResponse, ResponseResult.ok(userTokenVO));
log.info("user:{} token:{}", user.getUsername(), token);
}
}
前端登录请求的方法
methods: {
handleLogin: function () {
axios.post("http://localhost:8080/login", this.qs.stringify({
"username": this.username,
"password": this.password
})).then(res => {
if (res.data.status == "OK") {
//如果登录成功,就获得token,保存到本地
localStorage.setItem("username", res.data.data.username);
localStorage.setItem("token", res.data.data.token);
//跳转到user页面
this.$router.push({path: "/index"});
} else {
this.msg = "账号或密码错误!";
}
});
}
}
main.js
//配置axios拦截请求,添加token头信息
axios.interceptors.request.use(
config => {
let token = localStorage.getItem("token");
console.log("token:" + token);
if (token) {
//把localStorage的token放在Authorization里
config.headers.Authorization = token;
}
return config;
},
function(err) {
console.log("失败信息" + err);
}
);
//错误响应拦截
axios.interceptors.response.use(res => {
console.log('拦截响应');
console.log(res);
if( res.data.status === 'OK'|| res.status == 200){
return res;
}
if( res.data.data === '验证错误,需要登录' ){
console.log('验证错误,需要登录')
// window.location.href = '/'
MessageBox.alert('没有权限,需要登录','权限错误',{
confirmButtonText:'跳转登录页面',
callback: action => {
window.location.href = '/'
}
})
}else{
Message.error(res.data.data)
}
})
后端过滤器的Token解析验证
@Slf4j
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
public static final String HEADER = "Authorization";
public TokenAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获得前端请求中的token
String token = request.getHeader(HEADER);
if(StringUtils.isBlank(token)){
token = request.getParameter(HEADER);
}
//如果token为空,放行,验证失败
if(StringUtils.isBlank(token)){
chain.doFilter(request,response);
return;
}
try {
//解析token
String username = JwtUtil.getUsernameFromToken(token, RsaUtil.publicKey);
if (StringUtils.isNotBlank(username)) {
//把用户token放入SecurityContext,通过验证
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("");
User user = new User(username, "", authorities);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}catch (ExpiredJwtException e){
log.error("token过期",e);
}catch (Exception ex){
log.error("token解析错误",ex);
}
chain.doFilter(request,response);
}
}
单体应用的总体登录业务流程代码实现完成
存在的问题?
分布式架构存在多个服务,无法实现每个服务之间的登录验证
如何解决?
这里就需要使用到单点登录来替代原有的单体应用登录
实现方案:
1、JWT+Gateway方案
2、OAuth2方案
3、共享Session
这里我们使用JWT+Gateway方案来实现单点登录
JWT+Gateway单点登录的业务流程图:
具体实现步骤:
1、用户发送登录请求到达网关,登录请求无需拦截直接放行
2、访问用户服务,进行用户信息的校验
3、校验成功执行登录成功处理器,使用私钥加密生成JWT的Token并返回给客户端;校验失败则返回登录失败信息
4、客户端接收到Token以后,保存在localStorage中,跳转到登录后的页面
5、客户端再次发送请求,在通过前端请求拦截器的时候,会把localStorage的Token放在Authorization里
6、通过网关会进行Token信息的验证,验证成功放行,路由到指定服务,失败则返回错误信息,需重新进行登录
实现流程
- 创建用户数据库,用户表
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80026
Source Host : localhost:3306
Source Schema : edu_user
Target Server Type : MySQL
Target Server Version : 80026
File Encoding : 65001
Date: 21/08/2022 16:57:07
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`realname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`telephone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`state` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', '$2a$10$MieFvgTED163OIzgY3.sVO/lkCYDx02r705dtS.98Ff25zHpqh4Hy', '百里', '15667778888', NULL, NULL);
INSERT INTO `user` VALUES (2, 'user0', '$2a$10$MieFvgTED163OIzgY3.sVO/lkCYDx02r705dtS.98Ff25zHpqh4Hy', '恒哥', '15643435352', 'http://192.168.1.114:8848/nacos/img/logo-2000-390.svg', NULL);
INSERT INTO `user` VALUES (3, 'user1', '$2a$10$MieFvgTED163OIzgY3.sVO/lkCYDx02r705dtS.98Ff25zHpqh4Hy', 'xx', NULL, NULL, NULL);
SET FOREIGN_KEY_CHECKS = 1;
- 在父项目下创建用户服务,并继承父项目,添加依赖,注册到Nacos上
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 编写用户服务的配置
- 注册到Nacos上编写的配置文件
server.port=8010
spring.application.name=user-service
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/edu_user?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.type-aliases-package=com.blb.user_service.entity
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- 本地编写的配置文件
# nacos的地址,默认端口是8848
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# 配置文件的后缀
spring.cloud.nacos.config.file-extension=properties
# 配置文件的前缀
spring.cloud.nacos.config.prefix=user-service
# 使用的profile
spring.profiles.active=dev
- 在父项目下创建网关服务,并继承父项目,添加依赖,注册到Nacos上
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
- 编写网关服务的配置
- 注册到Nacos上编写的配置文件
server:
port: 9000
spring:
application:
name: gateway-service
cloud:
gateway:
routes: # 路由
- id: order-service-route #路由id
uri: lb://order-service #lb(注册中心中服务名字)
predicates:
- Path=/order/** # 断言,路径相匹配的进行路由
- id: product-service-route #路由id
uri: lb://product-service #lb(注册中心中服务名字)
predicates:
- Path=/product/** # 断言,路径相匹配的进行路由
- id: ad-service-route #路由id
uri: lb://ad-service #lb(注册中心中服务名字)
predicates:
- Path=/ad/**,/space/** # 断言,路径相匹配的进行路由
- id: user-service-route #路由id
uri: lb://user-service #lb(注册中心中服务名字)
predicates:
- Path=/login,/logout,/user/** # 断言,路径相匹配的进行路由
globalcors:
cors-configurations: # 跨域配置
'[/**]': # 匹配所有路径
allowed-origins: # 允许的域名
- "http://localhost:8080"
allowed-headers: "*" # 允许的请求头
allowed-methods: "*" # 允许的方法
allow-credentials: true # 是否携带cookie
user:
white-list: #拦截放行白名单
- /login
- /logout
- /space
- /user
- 本地编写的配置文件
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # nacos的地址,端口默认是8848
config:
server-addr: 127.0.0.1:8848 # 配置中心地址
file-extension: yml # 配置文件的后缀
prefix: gateway-service # 配置文件的前缀
profiles:
active: dev # 使用的profile
- 编写用户服务的SpringSecurity相关配置
package com.blb.user_service.config;
import com.blb.user_service.utils.ResponseResult;
import com.blb.user_service.utils.ResponseStatus;
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.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
/**
* SpringSecurity相关配置
*
* @author dell
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Qualifier("userDetailsServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
/**
* 密码加密器
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 设置自定义登录逻辑
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 页面资源的授权
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置放行URL
http.authorizeRequests()
.antMatchers("/swagger-ui.html", "/v2/**", "/swagger-resources/**",
"/webjars/springfox-swagger-ui/**", //放行swagger相关
"/js/**", "/css/**", //放行静态资源
"/login", "logout" //放行登录和登出
)
.permitAll()
.anyRequest()
.authenticated() //其它的请求需要登录
.and()
.formLogin()//登录配置
.successHandler(loginSuccessHandler) //登录成功处理
.failureHandler((req, resp, auth) -> { //配置登录失败的处理器
ResponseResult.write(resp, ResponseResult.error(ResponseStatus.LOGIN_ERROR));
})
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, auth) -> { //未进行登录请求的处理
ResponseResult.write(resp, ResponseResult.error(ResponseStatus.AUTH_ERROR));
})
.and()
.logout()//登出配置
.logoutSuccessHandler((req, resp, auth) -> { //配置登出的处理器
ResponseResult.write(resp, ResponseResult.ok("注销成功"));
})//退出登录处理
.clearAuthentication(true) //清除验证缓存
.and()
.csrf()
.disable() //关闭CSRF保护
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); //不使用session
}
}
- 编写用户服务的登录验证过滤器
package com.blb.user_service.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.blb.user_service.entity.User;
import com.blb.user_service.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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;
/**
* 登录验证过滤器
* @author dell
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户名查询用户信息
User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getUsername, s));
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
//返回正确的用户信息
return new org.springframework.security.core.userdetails.User(s, user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
- 编写用户服务的登录成功处理器
package com.blb.user_service.config;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.blb.user_service.entity.VO.UserTokenVO;
import com.blb.user_service.mapper.UserMapper;
import com.blb.user_service.utils.JwtUtil;
import com.blb.user_service.utils.ResponseResult;
import com.blb.user_service.utils.RsaUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author dell
*
* 登录成功处理器
*/
@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private UserMapper userMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
//获取用户信息
User user = (User) authentication.getPrincipal();
//将用户名生成JWT token
String token = JwtUtil.generateToken(user.getUsername(), RsaUtil.privateKey, JwtUtil.EXPIRE_MINUTES);
com.blb.user_service.entity.User user2 = userMapper.selectOne(new QueryWrapper<com.blb.user_service.entity.User>().lambda().eq(com.blb.user_service.entity.User::getUsername, user.getUsername()));
UserTokenVO userTokenVO = new UserTokenVO(user2, token);
//将token发送给前端
ResponseResult.write(httpServletResponse, ResponseResult.ok(userTokenVO));
log.info("user:{} token:{}", user.getUsername(), token);
}
}
- 编写用户服务的封装用户信息实体类
package com.blb.user_service.entity.VO;
import com.blb.user_service.entity.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author dell
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserTokenVO {
private User user;
private String token;
}
- JWT工具类、RSA工具类、响应状态枚举、响应数据封装对象的代码与单体应用业务流程代码类似,不过多赘述
- 编写网关服务的放行白名单配置类
package com.blb.gateway_service.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* @author dell
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "user")
public class WhiteListConfig {
/**
* 放行白名单集合
*/
private List<String> whiteList;
}
- 编写网关服务的用户请求验证过滤器
package com.blb.gateway_service.filter;
import com.blb.gateway_service.config.WhiteListConfig;
import com.blb.gateway_service.utils.JwtUtil;
import com.blb.gateway_service.utils.RsaUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 用户请求验证过滤器
*
* @author dell
*/
@Slf4j
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Autowired
private WhiteListConfig whiteListConfig;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获得请求和响应对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//对于白名单的路径放行
List<String> whiteList = whiteListConfig.getWhiteList();
for (String path : whiteList) {
if (request.getURI().getPath().contains(path)) {
log.info("{}白名单,放行", request.getURI().getPath());
return chain.filter(exchange);
}
}
//获取请求头中的token信息
String token = request.getHeaders().getFirst("Authorization");
try {
//解析token获取用户名
String username = JwtUtil.getUsernameFromToken(token, RsaUtil.publicKey);
log.info("{}解析成功,放行{}", username, request.getURI().getPath());
return chain.filter(exchange);
} catch (Exception e) {
e.printStackTrace();
log.error("token解析失败", e);
//返回未经授权状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
DataBuffer wrap = response.bufferFactory().wrap("验证错误,需要登录".getBytes());
return response.writeWith(Mono.just(wrap));
}
}
@Override
public int getOrder() {
return 0;
}
}
- 前端登录请求的方法
login() { // 前去登录
this.axios.post("/login", this.qs.stringify({username: this.username, password: this.password}))
.then((result) => {
console.log(result)
if (result.data.status == "OK") {
console.log("登录成功" + result.data.data.token);
this.dialogFormVisible = false; //关闭登录框
this.user = result.data.data.user; // 保存返回数据中的用户对象信息
this.isLogin = true; // 更新登录状态
localStorage.setItem("user", JSON.stringify(this.user)); // 将登录成功的对象信息保存到本地储存中
localStorage.setItem("token", result.data.data.token); //保存token
} else {
this.$message.error("登录失败!");
}
// this.$router.go(0);//刷新页面
}).catch((error) => {
this.$message.error("登录失败!");
this.dialogFormVisible = false; //关闭登录框
});
},
- main.js
//基础路径指向微服务网关
axios.defaults.baseURL="http://localhost:9000";
//允许带cookie
axios.defaults.withCredentials=true;
//配置axios拦截请求,添加token头信息
axios.interceptors.request.use(
config => {
let token = localStorage.getItem("token");
console.log("token:" + token);
if (token) {
//把localStorage的token放在Authorization里
config.headers.Authorization = token;
}
return config;
},
function(err) {
console.log("失败信息" + err);
}
);