SpringBoot3整合SpringSecurity,实现自定义接口权限过滤(一)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: SpringBoot3整合SpringSecurity,实现自定义接口权限过滤

一、导入依赖

在实现自定义接口权限过滤之前,首先要导入依赖,首先是 SpringBoot 父依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.0</version>
</parent>

接下来是 Spring Boot Web 依赖,用于提供最基本的接口支持。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

在实现登录时需要设计 Token,整合 Redis,所以需要加上以下依赖。

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.21.3</version>
</dependency>

最后就是 Spring Security 依赖,用于实现权限控制

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Boot 3.1 版本对应的 Spring Security 依赖为 6.1.0 版本,废弃了 WebSecurityConfigurerAdapter 类,配置内容和 Spring Security 5 有着明显不同,版本依赖如下图所示。


二、编写登录提示接口

要实现自定义接口权限过滤,首先要定义一个登录提示接口,用于被拦截时返回用户的数据,如下图所示。

请同学们新建一个控制器 SecurityController,定义这个 /zwz/common/needLogin 接口,代码如下。

@RestController
@RequestMapping("/zwz/common")
@Api(tags = "公共接口")
@Transactional
public class SecurityController {
    @RequestMapping(value = "/needLogin", method = RequestMethod.GET)
    @ApiOperation(value = "未登录返回的数据")
    public Result<Object> needLogin(){
        return ResultUtil.error(401, "登录失效");
    }
}

三、编写登录成功处理函数

很多同学会问,Spring Security项目中登录接口在哪里,如何实现登录功能,此时…

在 Spring Security 中,我们只需要对登录接口、登录成功/失败回调、过滤器等内容进行配置即可,开发者无需关注登录的具体实现。

请同学们新建 AuthenticationSuccessHandler 类,继承于 Spring Security 的 SavedRequestAwareAuthenticationSuccessHandler 类 ,用于回调用户登录成功的方法,。

@ApiOperation(value = "登录成功回调")
@Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
}

3.1 判断是否保存登录

请同学们首先重写 AuthenticationSuccessHandler 类中的 onAuthenticationSuccess 方法,这个方法用于实现登录成功回调

@Override
@ApiOperation(value = "登录成功回调")
@SystemLog(about = "登录系统", type = LogType.LOGIN)
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication ac) throws IOException, ServletException {
}

在实现登录成功回调的开始,首先要判断用户是否勾选了自动登录,获取是否登录的代码如下。

String saveLogin = request.getParameter(ZwzLoginProperties.SAVE_LOGIN_PRE);
Boolean saveLoginFlag = false;
if(!ZwzNullUtils.isNull(saveLogin) && Objects.equals(saveLogin,"true")){
    saveLoginFlag = true;
}

这样就可以把用户是否保存登录的标识保存在 saveLoginFlag 变量中,以备后续保存 Token 使用。

3.2 保存用户的信息和菜单权限

用户登录成功后,需要加载用户的菜单,此时需要把用户的账号和菜单保存到缓存中

用户第二次免登进入系统,即可快速从缓存中获取菜单数据,加快用户的菜单加载速度,降低数据库的读取压力

首先定义一个 TokenUser 类,用于存储临时用户信息(账号、菜单权限),代码如下。

@ApiOperation(value = "临时用户类")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TokenUser implements Serializable{
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "用户名")
    private String username;
    @ApiModelProperty(value = "拥有的菜单权限")
    private List<String> permissions;
    @ApiModelProperty(value = "是否自动登录")
    private Boolean saveLogin;
}

接着继续在 onAuthenticationSuccess 方法中实现菜单拉取。

List<String> permissionsList = new ArrayList<>();
List<GrantedAuthority> authorities = (List<GrantedAuthority>) ((UserDetails)ac.getPrincipal()).getAuthorities();
for(GrantedAuthority g : authorities){
    permissionsList.add(g.getAuthority());
}
String username = ((UserDetails)ac.getPrincipal()).getUsername();
TokenUser user = new TokenUser(username, permissionsList, saveLoginFlag);

上述代码将指定用户的菜单权限拉取出来,保存到 TokenUser 临时用户类中,以备后续调用。

3.3 单点登录处理

单点登录(SSO)是一种身份认证的技术或协议,允许用户在多个应用系统中使用同一组凭据(例如用户名和密码),只需进行一次身份验证即可访问所有的应用系统,从而实现了不同应用系统之间的身份认证信息共享。这种技术可以提高用户的使用便利性,避免重复登录,减少密码管理负担,同时也能够增强系统的安全性,降低密码泄露和被攻击的风险。

对于一般的管理系统来说,都支持单点登录。

通俗点来说,单点登录就是单个账号只允许一个地点登录,电脑 A 登录时,如果登录到电脑 B,此时电脑 A 会被 “顶” 下线。

接着继续在 onAuthenticationSuccess 方法中实现单点登录。

public static final String HTTP_TOKEN_PRE = "ZWZ_TOKEN_PRE:";
public static final String USER_TOKEN_PRE = "ZWZ_USER_TOKEN:";
String oldToken = redisTemplate.opsForValue().get(ZwzLoginProperties.USER_TOKEN_PRE + username);
if(StrUtil.isNotBlank(oldToken)){
    redisTemplate.delete(ZwzLoginProperties.HTTP_TOKEN_PRE + oldToken);
}

如果老的 Token 还存在,就把老 Token 删除,即可实现单点登录功能。

3.4 持久化登录信息

最后,将用户的数据持久化到 Redis 中,将 Token 返回给前端,存储到 Cookie 中,前端就可以使用 Token 免登进入、访问系统。

首先是 Token 的生成,可以采用 UUID 类辅助生成,代码如下。

String token = UUID.randomUUID().toString();

注入 Redis 工具类,代码如下。

import org.springframework.data.redis.core.StringRedisTemplate;
@Autowired
private StringRedisTemplate redisTemplate;

接着根据 3.1 步中的 是否保存 结果,进行持久化的处理,同学们可以自定义 Token 的前缀 USER_TOKEN_PRE

public static final String HTTP_TOKEN_PRE = "ZWZ_TOKEN_PRE:";
public static final String USER_TOKEN_PRE = "ZWZ_USER_TOKEN:";
if(saveLoginFlag){
    redisTemplate.opsForValue().set(ZwzLoginProperties.USER_TOKEN_PRE + username, token, 30, TimeUnit.DAYS);
    redisTemplate.opsForValue().set(ZwzLoginProperties.HTTP_TOKEN_PRE + token, JSON.toJSONString(user), 30, TimeUnit.DAYS);
}else{
    redisTemplate.opsForValue().set(ZwzLoginProperties.USER_TOKEN_PRE + username, token, 60, TimeUnit.MINUTES);
    redisTemplate.opsForValue().set(ZwzLoginProperties.HTTP_TOKEN_PRE + token, JSON.toJSONString(user), 60, TimeUnit.MINUTES);
}
ResponseUtil.out(response, ResponseUtil.resultMap(true, 200, "登录成功", token));

当用户勾选了保存登录,系统保存 Token 30天,即用户可以在未来 30 天内,免登进入系统。

如果用户没有勾选保存登录,系统保存 Token 60 分钟,即用户可以在未来 1 小时内,免登进入系统。

完整代码如下。

@ApiOperation(value = "登录成功回调")
@Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private ZwzLoginProperties tokenProperties;
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final boolean RESPONSE_SUCCESS_FLAG = true;
    private static final int RESPONSE_SUCCESS_CODE = 200;
    private static final String TOKEN_REPLACE_STR_FRONT = "-";
    private static final String TOKEN_REPLACE_STR_BACK = "";
    @Override
    @ApiOperation(value = "登录成功回调")
    @SystemLog(about = "登录系统", type = LogType.LOGIN)
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication ac) throws IOException, ServletException {
        String saveLogin = request.getParameter(ZwzLoginProperties.SAVE_LOGIN_PRE);
        Boolean saveLoginFlag = false;
        if(!ZwzNullUtils.isNull(saveLogin) && Objects.equals(saveLogin,"true")){
            saveLoginFlag = true;
        }
        List<String> permissionsList = new ArrayList<>();
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) ((UserDetails)ac.getPrincipal()).getAuthorities();
        for(GrantedAuthority g : authorities){
            permissionsList.add(g.getAuthority());
        }
        String token = UUID.randomUUID().toString().replace(TOKEN_REPLACE_STR_FRONT, TOKEN_REPLACE_STR_BACK);
        String username = ((UserDetails)ac.getPrincipal()).getUsername();
        TokenUser user = new TokenUser(username, permissionsList, saveLoginFlag);
        // 判断是否存储菜单权限
        if(!tokenProperties.getSaveRoleFlag()){
            user.setPermissions(null);
        }
        // 单点登录判断
        if(tokenProperties.getSsoFlag()){
            String oldToken = redisTemplate.opsForValue().get(ZwzLoginProperties.USER_TOKEN_PRE + username);
            if(StrUtil.isNotBlank(oldToken)){
                redisTemplate.delete(ZwzLoginProperties.HTTP_TOKEN_PRE + oldToken);
            }
        }
        if(saveLoginFlag){
            redisTemplate.opsForValue().set(ZwzLoginProperties.USER_TOKEN_PRE + username, token, tokenProperties.getUserSaveLoginTokenDays(), TimeUnit.DAYS);
            redisTemplate.opsForValue().set(ZwzLoginProperties.HTTP_TOKEN_PRE + token, JSON.toJSONString(user), tokenProperties.getUserSaveLoginTokenDays(), TimeUnit.DAYS);
        }else{
            redisTemplate.opsForValue().set(ZwzLoginProperties.USER_TOKEN_PRE + username, token, tokenProperties.getUserTokenInvalidDays(), TimeUnit.MINUTES);
            redisTemplate.opsForValue().set(ZwzLoginProperties.HTTP_TOKEN_PRE + token, JSON.toJSONString(user), tokenProperties.getUserTokenInvalidDays(), TimeUnit.MINUTES);
        }
        ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_SUCCESS_FLAG,RESPONSE_SUCCESS_CODE,"登录成功", token));
    }
}

四、编写登录失败处理函数

请同学们新建 AuthenticationFailHandler 类,继承于 Spring Security 的 SimpleUrlAuthenticationFailureHandler 类 ,用于回调用户登录失败 的方法,。

@ApiOperation(value = "登录失败回调")
@Slf4j
@Component
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
}

4.1 判断是否密码错误

请同学们首先重写 AuthenticationFailHandler 类中的 onAuthenticationFailure 方法,这个方法用于实现登录失败回调

@Override
@ApiOperation(value = "登录失败回调")
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
}

在实现登录失败回调的开始,首先要判断用户是否输入了错误的密码。

其中错误的密码包括:

  1. 用户密码输入错误。(UsernameNotFoundException 异常)
  2. 用户输入了正确的密码,但没有加密。(BadCredentialsException 异常)
if (exception instanceof BadCredentialsException || exception instanceof UsernameNotFoundException) {
    recordLoginTime(request.getParameter("username:"));
    String failTimesStr = stringRedisTemplate.opsForValue().get("LOGIN_FAIL_TIMES_PRE:" + request.getParameter("username:"));
    // 已错误的次数
    int userFailTimes = 0;
    if(!ZwzNullUtils.isNull(failTimesStr)){
        userFailTimes = Integer.parseInt(failTimesStr);
    }
    int restLoginTime = 10 - userFailTimes;
    if(restLoginTime < 5 && restLoginTime > 0){
        ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"账号密码不正确,还能尝试登录" + restLoginTime + "次"));
    } else if(restLoginTime < 1) {
        ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"重试超限,请您" + 10 + "分后再登录"));
    } else {
        ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"账号密码不正确"));
    }
}

默认设定 10 次登录试错次数,如果超限会被临时禁止登录

其中 recordLoginTime 方法用于查询登录失败的次数,代码如下。

@ApiOperation(value = "查询登录失败的次数")
public boolean recordLoginTime(String username) {
    String loginFailTimeStr = stringRedisTemplate.opsForValue().get(LOGIN_FAIL_TIMES_PRE + username);
    int loginFailTime = 0;
    // 已错误次数
    if(!ZwzNullUtils.isNull(loginFailTimeStr)){
        loginFailTime = Integer.parseInt(loginFailTimeStr) + 1;
    }
    stringRedisTemplate.opsForValue().set("LOGIN_FAIL_TIMES_PRE:" + username, loginFailTime + "", 10, TimeUnit.MINUTES);
    if(loginFailTime >= 10){
        stringRedisTemplate.opsForValue().set("userLoginDisableFlag:"+username, "fail", 10, TimeUnit.MINUTES);
        return false;
    }
    return true;
}

4.2 判断是否系统鉴权失败

接着判断是否是自定义异常 ZwzAuthException如果属于自定义异常,则抛出自定义异常的信息,代码如下。

if (exception instanceof ZwzAuthException){
    ResponseUtil.out(response, ResponseUtil.resultMap(false,500,((ZwzAuthException) exception).getMsg()));
}

其中自定义异常定义如下。

@ApiOperation(value = "自定义异常")
public class ZwzAuthException extends InternalAuthenticationServiceException {
    private static final long serialVersionUID = 1L;
    private static final String DEFAULT_MSG = "系统鉴权失败";
    @ApiModelProperty(value = "异常消息内容")
    private String msg;
    public ZwzAuthException(String msg){
        super(msg);
        this.msg = msg;
    }
    public ZwzAuthException(){
        super(DEFAULT_MSG);
        this.msg = DEFAULT_MSG;
    }
    public ZwzAuthException(String msg, Throwable t) {
        super(msg, t);
        this.msg = msg;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
207 0
|
2月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
203 2
|
18天前
|
安全 Java 应用服务中间件
如何将Spring Boot应用程序运行到自定义端口
如何将Spring Boot应用程序运行到自定义端口
27 0
|
1月前
|
Java 开发者 Spring
精通SpringBoot:16个扩展接口精讲
【10月更文挑战第16天】 SpringBoot以其简化的配置和强大的扩展性,成为了Java开发者的首选框架之一。SpringBoot提供了一系列的扩展接口,使得开发者能够灵活地定制和扩展应用的行为。掌握这些扩展接口,能够帮助我们写出更加优雅和高效的代码。本文将详细介绍16个SpringBoot的扩展接口,并探讨它们在实际开发中的应用。
47 1
|
2月前
|
存储 安全 Java
|
2月前
|
存储 算法 安全
SpringBoot 接口加密解密实现
【10月更文挑战第18天】
|
2月前
|
监控 Java 开发者
掌握SpringBoot扩展接口:提升代码优雅度的16个技巧
【10月更文挑战第20天】 SpringBoot以其简化配置和快速开发而受到开发者的青睐。除了基本的CRUD操作外,SpringBoot还提供了丰富的扩展接口,让我们能够更灵活地定制和扩展应用。以下是16个常用的SpringBoot扩展接口,掌握它们将帮助你写出更加优雅的代码。
87 0
|
2月前
|
前端开发 Java 数据库
springBoot:template engine&自定义一个mvc&后端给前端传数据&增删改查 (三)
本文介绍了如何自定义一个 MVC 框架,包括后端向前端传递数据、前后端代理配置、实现增删改查功能以及分页查询。详细展示了代码示例,从配置文件到控制器、服务层和数据访问层的实现,帮助开发者快速理解和应用。
|
2月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
171 1
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
110 62