Spring Security单点登录

news2025/1/14 0:34:51

        本文介绍了Spring Security单点登录的概念和基本原理。单点登录是指用户只需登录一次,即可在多个相互信任的系统中实现无缝访问和授权。通过Spring Security框架的支持,可以实现有效的用户管理和权限控制。最后,本文提供了实际应用案例,展示了Spring Security单点登录的实际应用和效果。通过学习本文,读者可以了解并掌握Spring Security单点登录的基本概念和实现方法,从而提高系统的用户体验和安全性。

一、登录接口

1,登录流程

为什么需要先以用户名为key存储UUID,再以UUID为key存储JWT Token?
安全性:直接使用用户名为key存储JWT Token存在安全风险。因为用户名是用户公开的信息,如果Token存储方式与用户名直接关联,那么攻击者可能会尝试猜测或枚举用户名来获取Token。而UUID是一个随机生成的唯一标识符,不易被猜测,因此使用UUID作为key存储Token可以提高安全性。
解耦:将用户名和Token的存储解耦可以提高系统的灵活性和可扩展性。例如,如果未来需要更改用户名的存储方式或进行用户数据迁移,使用UUID作为Token的key可以确保Token的存储不受影响。
Token管理:使用UUID作为中间步骤还可以简化Token的管理。例如,如果需要重置Token或进行Token续期操作,系统可以方便地通过UUID找到对应的Token并进行处理。

2,接口地址

/**
 *  登录接口
 */
@RestController
@Api(tags = "用户登录")
@RequestMapping("security")
public class LoginController {

    @Autowired
    LoginService loginService;

    @PostMapping("login")
    @ApiOperation(value = "用户登录",notes = "用户登录")
    @ApiImplicitParam(name = "userVo",value = "登录对象",required = true,dataType = "UserVo")
    @ApiOperationSupport(includeParameters ={"userVo.username","userVo.password"} )
    public ResponseResult<UserVo> login(@RequestBody UserVo userVo){
        return ResponseResult.success(loginService.login(userVo));
    }
}

3,自定义认证用户

/**
 *  自定认证用户
 */
@Data
@NoArgsConstructor
public class UserAuth implements UserDetails {

    private String id;

    /**
     * 用户账号
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 权限内置
     */
    private Collection<SimpleGrantedAuthority> authorities;

    /**
     * 用户类型(00系统用户)
     */
    private String userType;

    /**
     * 用户昵称
     */
    private String nickName;

    /**
     * 用户邮箱
     */
    private String email;

    /**
     * 真实姓名
     */
    private String realName;

    /**
     * 手机号码
     */
    private String mobile;

    /**
     * 用户性别(0男 1女 2未知)
     */
    private String sex;

    /**
     * 创建者
     */
    private Long createBy;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新者
     */
    private Long updateBy;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 备注
     */
    private String remark;

    /**
     * 部门编号【当前】
     */
    private String deptNo;

    /**
     * 职位编号【当前】
     */
    private String postNo;

    public UserAuth(UserVo userVo) {
        this.setId(userVo.getId().toString());
        this.setUsername(userVo.getUsername());
        this.setPassword(userVo.getPassword());
        if (!EmptyUtil.isNullOrEmpty(userVo.getResourceRequestPaths())) {
            authorities = new ArrayList<>();
            userVo.getResourceRequestPaths().forEach(resourceRequestPath -> authorities.add(new SimpleGrantedAuthority(resourceRequestPath)));
        }
        this.setUserType(userVo.getUserType());
        this.setNickName(userVo.getNickName());
        this.setEmail(userVo.getEmail());
        this.setRealName(userVo.getRealName());
        this.setMobile(userVo.getMobile());
        this.setSex(userVo.getSex());
        this.setCreateTime(userVo.getCreateTime());
        this.setCreateBy(userVo.getCreateBy());
        this.setUpdateTime(userVo.getUpdateTime());
        this.setUpdateBy(userVo.getUpdateBy());
        this.setRemark(userVo.getRemark());
        this.setDeptNo(userVo.getDeptNo());
        this.setPostNo(userVo.getPostNo());
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

4,定义UserDetailService实现类

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserVo userVo = userService.findUserVoForLogin(username);
        return new UserAuth(userVo);
    }
}

5,登录接口逻辑实现

public UserVo login(LoginDto loginDto) {

        // 1.获取认证用户
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (!authenticate.isAuthenticated()) {
            throw new BaseException("登录失败");
        }
        UserAuth principal = (UserAuth) authenticate.getPrincipal();
        UserVo user = BeanUtil.copyProperties(principal, UserVo.class);

        // 2.获取当前角色列表
        List<RoleVo> roleList = roleService.findRoleVoListByUserId(user.getId());
        Set<String> roleLabels = roleList.stream().map(RoleVo::getLabel).collect(Collectors.toSet());
        user.setRoleLabels(roleLabels);

        // 3.获取用户资源表示
        List<ResourceVo> resourceList = resourceService.findResourceVoListByUserId(user.getId());
        Set<String> requestPaths = resourceList.stream()
                .filter(resourceVo -> resourceVo.getResourceType().equals("r"))
                .map(ResourceVo::getRequestPath)
                .collect(Collectors.toSet());
        user.setResourceRequestPaths(requestPaths);
        user.setPassword("");

        // 4.颁发Token
        Map<String, Object> claims = new HashMap<>();
        claims.put("currentUser", JSON.toJSONString(user));
        String token = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(),
                jwtTokenManagerProperties.getTtl(), claims);

        // 5.生成UUID存储Redis
        String uuid = UUID.randomUUID().toString();
        String userTokenKey = UserCacheConstant.USER_TOKEN+user.getUsername();
        long ttl = jwtTokenManagerProperties.getTtl() / 1000;
        redisTemplate.opsForValue().set(userTokenKey, uuid,
                ttl, TimeUnit.SECONDS);

        // 6.将Token存储Redis
        String jwtTokenKey = UserCacheConstant.JWT_TOKEN + uuid;
        redisTemplate.opsForValue().set(jwtTokenKey, token,
                ttl, TimeUnit.SECONDS);

        // 7.返回
        user.setUserToken(uuid);
        return user;
    }

二、自定义授权管理器

1,定义管理器

@Component
@Slf4j
public class JwtAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private JwtTokenManagerProperties jwtTokenManagerProperties;

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication,
                                       RequestAuthorizationContext requestContext) {
        // 1.获取jwtToken
        String userUUID = requestContext.getRequest().getHeader(SecurityConstant.USER_TOKEN);
        if (EmptyUtil.isNullOrEmpty(userUUID)){
            return new AuthorizationDecision(false);
        }

        String jwtTokenKey = UserCacheConstant.JWT_TOKEN+userUUID;
        String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);
        if (EmptyUtil.isNullOrEmpty(jwtToken)){
            return new AuthorizationDecision(false);
        }

        // 2.解析jwtToken
        String userJSON = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(),
                jwtToken).get("currentUser").toString();
        UserVo userVo = JSON.parseObject(userJSON, UserVo.class);
        if (EmptyUtil.isNullOrEmpty(userVo.getUsername())) {
            return new AuthorizationDecision(false);
        }

        // 3.判断uuid是否相同
        String userTokenKey = UserCacheConstant.USER_TOKEN + userVo.getUsername();
        String uuid = redisTemplate.opsForValue().get(userTokenKey);
        if (Objects.equals(uuid, userUUID)) {
            return new AuthorizationDecision(false);
        }

        // 4.重置过期时间
        Long expire = redisTemplate.opsForValue().getOperations()
                .getExpire(jwtTokenKey);
        if (expire.longValue() <= 600) {
            //jwt生成的token也会过期,故需要再次生成
            Map<String, Object> claims = new HashMap<>();
            claims.put("currentUser", JSON.toJSONString(userVo));
            String token = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(),
                    jwtTokenManagerProperties.getTtl(), claims);
            long ttl = jwtTokenManagerProperties.getTtl() / 1000;

            redisTemplate.opsForValue().set(jwtTokenKey, token, ttl, TimeUnit.SECONDS);
            redisTemplate.expire(userTokenKey, ttl, TimeUnit.SECONDS);
        }
        return new AuthorizationDecision(true);
    }
}

2,权限配置

/**
 *  权限核心配置类
 */
@Configuration
@EnableConfigurationProperties(SecurityConfigProperties.class)
public class SecurityConfig  {

    @Autowired
    SecurityConfigProperties securityConfigProperties;

    @Autowired
    JwtAuthorizationManager jwtAuthorizationManager;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        //忽略地址
        List<String> ignoreUrl = securityConfigProperties.getIgnoreUrl();
        http.authorizeHttpRequests()
                .antMatchers( ignoreUrl.toArray( new String[ignoreUrl.size() ] ) )
                .permitAll()
                .anyRequest().access(jwtAuthorizationManager);

        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS );//关闭session
        http.headers().cacheControl().disable();//关闭缓存

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return  authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * BCrypt密码编码
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

三、解析用户

1,定义拦截器

用户登录后获token,请求携token验证。用ThreadLocal存当前线程用户数据,实现线程隔离与数据共享

/**
 *  多租户放到SubjectContent上下文中
 */
@Component
public class UserTokenIntercept implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private JwtTokenManagerProperties jwtTokenManagerProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {


        String userUUID = request.getHeader(SecurityConstant.USER_TOKEN);
        if (!StringUtils.hasText(userUUID)) {
            throw new Exception("用户登录状态异常");
        }

        String jwtTokenKey = UserCacheConstant.JWT_TOKEN + userUUID;
        String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);
        if (!StringUtils.hasText(jwtToken)) {
            throw new Exception("用户登录状态异常");
        }

        Claims claims = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtToken);
        String user = claims.get("currentUser").toString();
        UserThreadLocal.setSubject(user);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.removeSubject();
    }
}

2,配置生效

/**
 *  webMvc高级配置
// */
@Configuration
@ComponentScan("springfox.documentation.swagger.web")
public class WebMvcConfig implements WebMvcConfigurer {

   @Autowired
   UserTokenIntercept userTokenIntercept;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {;
        registry.addInterceptor(userTokenIntercept).excludePathPatterns("/**");
    }

    /**
     * 资源路径 映射
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //支持webjars
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
        //支持swagger
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        //支持小刀
        registry.addResourceHandler("doc.html")
                .addResourceLocations("classpath:/META-INF/resources/");
    }

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return builder -> {
            // 序列化
            builder.serializerByType(Long.class, ToStringSerializer.instance);
            builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
            builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
            builder.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
            builder.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

            // 反序列化
            builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
            builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
            builder.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        };
    }
}

注意:执行流程会先经过授权管理器,当授权管理器放行通过才到拦截器生效

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2276218.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LKT4304新一代算法移植加密芯片,守护物联网设备和云服务安全

凌科芯安作为一家在加密芯片领域深耕18年的企业&#xff0c;主推的LKT4304系列加密芯片集成了身份认证、算法下载、数据保护和完整性校验等多方面安全防护功能&#xff0c;可以为客户的产品提供一站式解决方案&#xff0c;并且在调试和使用过程提供全程技术支持&#xff0c;针对…

浅谈云计算04 | 云基础设施机制

探秘云基础设施机制&#xff1a;云计算的基石 一、云基础设施 —— 云计算的根基![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1fb7ff493d3c4a1a87f539742a4f57a5.png)二、核心机制之网络&#xff1a;连接云的桥梁&#xff08;一&#xff09;虚拟网络边界&#xff…

Qt C++读写NFC标签NDEF网址URI

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.1d292c1biFgjSs&ftt&id615391857885 #include "mainwindow.h" #include "ui_mainwindow.h" #include <QDebug> #include "QLibrary" …

Java 如何传参xml调用接口获取数据

传参和返参的效果图如下&#xff1a; 传参&#xff1a; 返参&#xff1a; 代码实现&#xff1a; 1、最外层类 /*** 外层DATA类*/ XmlRootElement(name "DATA") public class PointsXmlData {private int rltFlag;private int failType;private String failMemo;p…

2024年度漏洞态势分析报告,需要访问自取即可!(PDF版本)

2024年度漏洞态势分析报告&#xff0c;需要访问自取即可!(PDF版本),大家有什么好的也可以发一下看看

不同音频振幅dBFS计算方法

1. 振幅的基本概念 振幅是描述音频信号强度的一个重要参数。它通常表示为信号的幅度值&#xff0c;幅度越大&#xff0c;声音听起来就越响。为了更好地理解和处理音频信号&#xff0c;通常会将振幅转换为分贝&#xff08;dB&#xff09;单位。分贝是一个对数单位&#xff0c;能…

Nginx反向代理请求头有下划线_导致丢失问题处理

后端发来消息说前端已经发了但是后端没收到请求。 发现是下划线的都没收到&#xff0c;搜索之后发现nginx默认request的header中包含’_’时&#xff0c;会自动忽略掉。 解决方法是&#xff1a;在nginx里的nginx.conf配置文件中的http部分中添加如下配置&#xff1a; unders…

C语言程序环境和预处理详解

本章重点&#xff1a; 程序的翻译环境 程序的执行环境 详解&#xff1a;C语言程序的编译链接 预定义符号介绍 预处理指令 #define 宏和函数的对比 预处理操作符#和##的介绍 命令定义 预处理指令 #include 预处理指令 #undef 条件编译 程序的翻译环境和执行环…

计算机组成原理(1)

系统概述 计算机硬件基本组成早期冯诺依曼机现代计算机 计算机各部分工作原理主存储器运算器控制器计算机工作过程 此文章的图片资源获取来自于王道考研 计算机硬件基本组成 早期冯诺依曼机 存储程序是指将指令以二进制的形式事先输入到计算机的主存储器&#xff0c;然后按照…

基于element UI el-dropdown打造表格操作列的“更多⌵”上下文关联菜单

<template><div :class"$options.name"><el-table :data"tableData"><el-table-column type"index" label"序号" width"60" /><!-- 主要列 BEGIN---------------------------------------- --&g…

Oracle 表分区简介

目录 一. 前置知识1.1 什么是表分区1.2 表分区的优势1.3 表分区的使用条件 二. 表分区的方法2.1 范围分区&#xff08;Range Partitioning&#xff09;2.2 列表分区&#xff08;List Partitioning&#xff09;2.3 哈希分区&#xff08;Hash Partitioning&#xff09;2.4 复合分…

罗永浩再创业,这次盯上了 AI?

罗永浩&#xff0c;1972年7月9日生于中国延边朝鲜族自治州的一个军人家庭&#xff0c;是一名朝鲜族人&#xff1b;早年在新东方授课&#xff0c;2004年当选 “网络十大红人” &#xff1b;2006年8月1日&#xff0c;罗永浩创办牛博网&#xff1b;2008年5月&#xff0c;罗永浩注册…

自然语言处理基础:全面概述

自然语言处理基础&#xff1a;全面概述 什么是NLP及其重要性、NLP的核心组件、NLU与NLG、NLU与NLG的集成、NLP的挑战以及NLP的未来 自然语言处理&#xff08;NLP&#xff09;是人工智能&#xff08;AI&#xff09;中最引人入胜且具有影响力的领域之一。它驱动着我们日常使用的…

WPF系列八:图形控件Path

简介 Path控件支持一种称为路径迷你语言&#xff08;Path Mini-Language&#xff09;的紧凑字符串格式&#xff0c;用于描述复杂的几何图形。这种语言通过一系列命令字母和坐标来定义路径上的点和线段&#xff0c;最终绘制出想要的图形。 绘制任意形状&#xff1a;可以用来绘…

计算机图形学【绘制立方体和正六边形】

工具介绍 OpenGL&#xff1a;一个跨语言的图形API&#xff0c;用于渲染2D和3D图形。它提供了绘制图形所需的底层功能。 GLUT&#xff1a;OpenGL的一个工具库&#xff0c;简化了窗口创建、输入处理和其他与图形环境相关的任务。 使用的函数 1. glClear(GL_COLOR_BUFFER_BIT |…

有限元分析学习——Anasys Workbanch第一阶段笔记(10)桌子载荷案例分析_实际载荷与均布载荷的对比

目录 0 序言 1 桌子案例 2 模型简化 3 方案A 前处理 1&#xff09;分析类型选择 2&#xff09;材料加载 3&#xff09;约束、载荷及接触 4&#xff09;控制网格(网格大小需要根据结果不断调整) 初始计算结果 加密后计算结果 4 方案B、C 前处理 1&#xff09;分析…

Docker compose 使用 --force-recreate --no-recreate 控制重启容器时的行为【后续】

前情&#xff1a;上一篇实际是让AI工具帮我总结了一下讨论的内容&#xff0c;这里把讨论的过程贴出来&#xff0c;这个讨论是为解决实际问题 前文https://blog.csdn.net/wgdzg/article/details/145039446 问题说明&#xff1a; 我使用 docker compose 管理我的容器&#xff0…

Mysql--基础篇--多表查询(JOIN,笛卡尔积)

在MySQL中&#xff0c;多表查询&#xff08;也称为联表查询或JOIN操作&#xff09;是数据库操作中非常常见的需求。通过多表查询&#xff0c;你可以从多个表中获取相关数据&#xff0c;并根据一定的条件将它们组合在一起。MySQL支持多种类型的JOIN操作&#xff0c;每种JOIN都有…

postgresql|数据库|利用sqlparse和psycopg2库批量按顺序执行SQL语句(psyconpg2新优化版本)

一、 旧版批量执行SQL脚本的python文件缺点&#xff0c;优点&#xff0c;以及更新内容 书接上回&#xff0c;postgresql|数据库开发|python的psycopg2库按指定顺序批量执行SQL文件(可离线化部署)_python sql psycopg2-CSDN博客 这个python脚本写了很久了&#xff0c;最近开始…

5个不同类型的数据库安装

各种社区版本下载官方地址&#xff1a;MySQL :: MySQL Community Downloads 一、在线YUM仓库&#xff08;Linux&#xff09; 选择 MySQL Yum Repository 选择对应版本下载仓库安装包&#xff08;No thanks, just start my download.&#xff09; 下载方法1&#xff1a;下载到本…