SpringCloud项目,使用JWT+Gateway方案,实现单点登录

什么是单点登录?

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

为什么要使用单点登录?

使用单点登录能够有效降低企业成本,提高员工工作效率,具体包括:
1、单点登录能够减少密码输入疲劳
2、单点登录通过减少登录次数来提高工作效率
3、单点登录为系统管理人员工作提供便利
4、单点登录能够提高系统安全性
5、单点登录简化了用户跟踪审计过程

对比单体应用的登录

单体应用登录的总体业务流程图:
在这里插入图片描述

基于JWT和RSA的Token机制

JWT简介

JSON Web Tokens 是JSON格式的加密字符串,用于加密验证信息,在前后端进行通信

分为三个部分

1、头部
2、负载
3、指纹

JWT官方网站

在这里插入图片描述

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);
    }
);

单点登录的总体登录业务流程代码实现完成

  • 9
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Spring Security和JWT(JSON Web Token)是两个不同的概念和技术,但可以结合使用实现单点登录Spring Security是一个强大的身份验证和访问控制框架,用于保护应用程序的安全性。它提供了一组功能丰富的工具和库,用于处理认证、授权和安全配置等方面的任务。 JWT是一种用于在网络应用中传递信息的开放标准(RFC 7519)。它由三部分组成:头部、载荷和签名。头部包含有关生成和验证JWT的元数据,载荷包含有关用户或其他实体的信息,签名用于验证JWT的完整性。 单点登录(SSO)是一种身份验证机制,允许用户使用一组凭据(例如用户名和密码)登录到一个系统后,即可无需再次输入凭据即可访问其他系统。 要实现基于Spring Security和JWT单点登录,可以遵循以下步骤: 1. 用户成功登录到主系统(例如系统A),该系统生成并返回一个JWT给用户。 2. 用户尝试访问其他受保护的系统(例如系统B)时,在请求中包含JWT。 3. 系统B接收到请求后,使用公钥验证JWT的签名,并解析出用户信息。 4. 如果JWT验证成功且用户有权限访问系统B,则允许用户访问系统B。 需要注意的是,要实现真正的单点登录,还需要一个中心化的身份验证系统(例如OAuth2认证服务器),用于生成和验证JWT,并为不同的系统颁发相同的JWT。 此外,还可以使用Spring Security提供的一些配置来简化JWT的集成,例如自定义认证过滤器、授权管理器和认证提供者等。 总结起来,Spring Security和JWT可以结合使用实现单点登录Spring Security负责处理认证和授权,JWT用于传递用户信息和验证用户身份。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值