Spring Security单点登录

        本文介绍了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)));

        };
    }
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

查找某个年龄段的用户信息TCP头格式为什么需要 TCP 协议? TCP 工作在哪一层?

查找某个年龄段的用户信息 select device_id,gender,age from user_profile where age>20 and age<23; TCP头格式 序列号&#xff1a;在建立连接时由计算机生成的随机数作为其初始值&#xff0c;通过 SYN 包传给接收端主机&#xff0c;每发送一次数据&#xff0c;就「累…

后端技术选型 sa-token校验学习 下 结合项目学习 后端鉴权

目录 后端注册拦截器 实现对 WebMvcConfigurer 接口的类实现 静态变量 方法重写 注册 Spring Framework拦截器 Sa-Token中SaServletFilter拦截器 思考 为什么使用两个拦截器 1. Spring Framework 拦截器 2. SaServletFilter 为什么要注册两个拦截器&#xff1f; 总结 …

使用 Charles 调试 Flutter 应用中的 Dio 网络请求

为了成功使用 Charles 抓取并调试 Flutter 应用程序通过 Dio 发起的网络请求&#xff0c;需遵循特定配置步骤来确保应用程序能够识别 Charles 的 SSL 证书&#xff0c;并正确设置代理服务器。 配置 Charles 以支持 HTTPS 请求捕获 Charles 默认会拦截 HTTP 流量&#xff1b;…

亿道三防丨三防笔记本是什么意思?和普通笔记本的优势在哪里?

三防笔记本是什么意思&#xff1f;和普通笔记本的优势在哪里&#xff1f; 在现代社会中&#xff0c;笔记本电脑已经成为人们工作和生活中不可或缺的一部分。然而&#xff0c;在一些特殊行业或环境中&#xff0c;普通笔记本电脑由于其脆弱性和对环境条件的敏感性&#xff0c;往…

Spring Boot教程之五十七:在 Apache Kafka 上发布 JSON 消息

Spring Boot | 如何在 Apache Kafka 上发布 JSON 消息 Apache Kafka是一个发布-订阅消息系统。消息队列允许您在进程、应用程序和服务器之间发送消息。在本文中&#xff0c;我们将了解如何在 Spring Boot 应用程序中向 Apache Kafka 发送 JSON 消息。 为了了解如何创建 Spring…

解决“无法定位程序输入点 av_buffer_create 于动态链接库 XXX\Obsidian.exe 上”问题

解决“无法定位程序输入点 av_buffer_create 于动态链接库 XXX\Obsidian.exe 上”问题 问题描述 本人在使用zotero中的zotero one&#xff08;青柠学术插件&#xff09;的时候&#xff0c;使用插件跳转obsidian中的对应笔记&#xff0c;出现上图情况。&#xff08;错误中提到的…

切削刀具热处理的作用学习笔记分享

对于一个搞冷加工的打工仔来说&#xff0c;热工的知识总是感觉那么新鲜。本期一起来学习一下切削刀具的热处理的一点点内容&#xff0c;虽然不是那么专业&#xff0c;但是了解一些还是很有好处的&#xff0c;废话不多说了&#xff0c;直接开始&#xff1a; 切削刀具热处理的作…

基于Web的宠物医院看诊系统设计与实现(源码+定制+开发)在线预约平台、宠物病历管理、医生诊疗记录、宠物健康数据分析 宠物就诊预约、病历管理与健康分析

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

WPS excel使用宏编辑器合并 Sheet工作表

使用excel自带的工具合并Sheet表&#xff0c;我们会发现需要开通WPS会员才能使用合并功能&#xff1b; 那么WPS excel如何使用宏编辑器进行合并 Sheet表呢&#xff1f; 1、首先我们要看excel后缀是 .xlsx 还是 .xls &#xff1b;如果是.xlsx 那么 我们需要修改为 .xls 注…

ubuntu22.4 ROS2 安装gazebo(环境变量配置)

ubuntu版本&#xff1a;ubuntu22.4 最近在学习ROS2 视频教程古月居的入门课&#xff1a; 视频教程 文字笔记 问题 在学到关于Gazebo的时候&#xff0c;遇到下面问题&#xff1a; 运行 $ ros2 launch gazebo_ros gazebo.launch.py在这里卡住&#xff0c;不弹出gazebo 解决…

【Linux】7.Linux基础开发工具使用(1)

文章目录 1. Linux 软件包管理器 yum1.1 什么是软件包1.2 关于 rzsz1.3 查看软件包1.4 如何安装软件1.5 如何卸载软件我怎么知道要安装什么软件呢&#xff1f;源常用命令对照表&#xff1a; 2. Linux开发工具Linux编辑器-vim使用2.1 vim的基本概念命令模式&#xff1a;2.2 简单…

漫话架构师|什么是系统架构设计师(开篇)

~犬&#x1f4f0;余~ “我欲贱而贵&#xff0c;愚而智&#xff0c;贫而富&#xff0c;可乎&#xff1f; 曰&#xff1a;其唯学乎” 关注犬余&#xff0c;共同进步 技术从此不孤单

DevOps实用场景:在哪些业务中应用DevOps最有效

随着科技的迅猛发展和客户需求的不断变化&#xff0c;IT初创公司在不断追求更高的效率、更快速的交付和更强的市场适应力。在这个背景下&#xff0c;DevOps成为了推动组织成功的关键策略之一。本文将帮助您了解什么是DevOps&#xff0c;哪些团队或企业最适合实施DevOps&#xf…

如何学习网络安全?有哪些小窍门?

学好网络安全其实没有所谓的捷径&#xff0c;也没有什么小窍门。 入门网络安全首先要有浓厚的学习兴趣&#xff0c;不然很容易就变成了从入门到放弃了。 其次要能静下心&#xff0c;踏踏实实的打好基础。如果你是零基础&#xff0c;建议从Web安全入手&#xff0c;课程难度相对…

Windows下载MySQL8.0

Windows下载MySQL8.0 MySQL :: Download MySQL Installer (Archived Versions) 在这个网页中选择相应的版本&#xff0c;点击下载即可。 但是在下载之前需要保证&#xff1a; 电脑用户名是英文相应的防火墙要关闭以前的mysql要在系统中卸载干净在控制命令里&#xff0c;以管…

使用Java Socket实现GPS定位数据处理

在许多应用场景中&#xff0c;如车辆追踪、移动设备定位等&#xff0c;GPS定位数据的实时获取和处理至关重要。本文将介绍如何使用Java Socket编程来接收GPS设备发送的数据并进行处理。 业务说明&#xff1a; 车辆追踪系统需要实时获取车辆的GPS定位信息。车辆上的GPS设备通过…

【Unity踩坑】Unity中提示缺少Visual Studio组件

问题&#xff1a; 在Unity中选择UWP平台时&#xff0c;提示Visual Studio缺少组件。 Selected Visual Studio is missing required components and may not be able to build the generated project. 解决方案&#xff1a; 在Visual Studio Installer里&#xff0c;安装上&quo…

ADC(Analog-to-digital converter)模拟-数字转换器

ADC简介 ADC&#xff08;Analog-to-Digital Converter&#xff09;&#xff0c;即模拟-数字转换器&#xff0c;是一种将模拟信号转换成数字信号的电子设备。它在现代电子系统中扮演着至关重要的角色&#xff0c;广泛应用于传感器信号处理、通信系统、医疗设备、工业自动化等多…

ASP.NET Core - 日志记录系统(二)

ASP.NET Core - 日志记录系统&#xff08;二&#xff09; 2.4 日志提供程序2.4.1 内置日志提供程序2.4.2 源码解析 本篇接着上一篇 ASP.NET Core - 日志记录系统(一) 往下讲&#xff0c;所以目录不是从 1 开始的。 2.4 日志提供程序 2.4.1 内置日志提供程序 ASP.NET Core 包括…

从零开始深度学习:(1)张量的常用操作

孩子们&#xff0c;懒大王回来了&#xff01; 正如标题所说&#xff0c;今天我们继续开始新的篇章&#xff0c;我们要开始高强度学习深度学习的相关内容&#xff0c;这个专栏内容较多、全是干货&#xff0c;我们还会在合适的地方进行拓展一些额外的语法或者别的相关知识&#…