SpringBoot整合Spring Security实现前后端分离登录权限处理

目录

前言

目录结构:

前期工作:

1.统一错误枚举

 2.统一json返回体

3.pom

4.配置文件

数据库设计

Spring Security核心配置:WebSecurityConfig

用户登录认证逻辑UserDetailsService

1.自定义实现类

2.创建自定义UserDetailsService

3.service和dao层方法

用户名密码加密

屏蔽Spring Security默认重定向登录页面以实现前后端分离功能

登录失败处理

实现登录成功/失败、登出处理逻辑

在WebSecurityConfig中的configure(HttpSecurity http)方法中声明

会话管理(登录过时、限制单用户或多用户登录等)

限制登录用户数量

处理账号被挤下线处理逻辑

在WebSecurityConfig中声明

实现基于JDBC的动态权限控制

权限拦截器

安全元数据源FilterInvocationSecurityMetadataSource

访问决策管理器AccessDecisionManager

在WebSecurityConfig中声明

最终的WebSecurityConfig配置


前言

首先要感谢这位博主得分享,根据他的博客,直接就可以依照参考将权限模块处理成功,这里我就不讲述spring Security的一些概念了,相信有的博主写的更好,或者直接去官网查看Spring Security,想看这位博主连接的直接跳转查看,原文连接:

Springboot + Spring Security 实现前后端分离登录认证及权限控制_I_am_Rick_Hu的博客-CSDN博客_springsecurity前后端分离登录认证

目录结构:

首先这是我的目录结构:

前期工作:

1.统一错误枚举

/**
 * @Author: Hutengfei
 * @Description: 返回码定义
 * 规定:
 * #1表示成功
 * #1001~1999 区间表示参数错误
 * #2001~2999 区间表示用户错误
 * #3001~3999 区间表示接口异常
 * @Date Create in 2019/7/22 19:28
 */
public enum ResultCode {
    /* 成功 */
    SUCCESS(200, "成功"),

    /* 默认失败 */
    COMMON_FAIL(999, "失败"),

    /* 参数错误:1000~1999 */
    PARAM_NOT_VALID(1001, "参数无效"),
    PARAM_IS_BLANK(1002, "参数为空"),
    PARAM_TYPE_ERROR(1003, "参数类型错误"),
    PARAM_NOT_COMPLETE(1004, "参数缺失"),

    /* 用户错误 */
    USER_NOT_LOGIN(2001, "用户未登录"),
    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
    USER_CREDENTIALS_ERROR(2003, "密码错误"),
    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
    USER_ACCOUNT_DISABLE(2005, "账号不可用"),
    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
    USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
    USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),

    /* 业务错误 */
    NO_PERMISSION(3001, "没有权限");
    private Integer code;
    private String message;

    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * 根据code获取message
     *
     * @param code
     * @return
     */
    public static String getMessageByCode(Integer code) {
        for (ResultCode ele : values()) {
            if (ele.getCode().equals(code)) {
                return ele.getMessage();
            }
        }
        return null;
    }
}

 2.统一json返回体

public class JsonResult<T> implements Serializable {
    private Boolean success;
    private Integer errorCode;
    private String errorMsg;
    private T data;

    public JsonResult() {
    }

    public JsonResult(boolean success) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
    }

    public JsonResult(boolean success, ResultCode resultEnum) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
    }

    public JsonResult(boolean success, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
        this.data = data;
    }

    public JsonResult(boolean success, ResultCode resultEnum, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
        this.data = data;
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(Integer errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
/**
 * @Author: zm
 * @Description:统一返回体构造工具
 * @Date: 2022/4/24 8:53
 */
public class ResultTool {
    public static JsonResult success() {
        return new JsonResult(true);
    }

    public static <T> JsonResult<T> success(T data) {
        return new JsonResult(true, data);
    }

    public static JsonResult fail() {
        return new JsonResult(false);
    }

    public static JsonResult fail(ResultCode resultEnum) {
        return new JsonResult(false, resultEnum);
    }
}

3.pom

   <properties>
        <java.version>11</java.version>
        <spring.security.version>5.1.6.RELEASE</spring.security.version>
        <fastjson.version>1.2.46</fastjson.version>
    </properties> 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>
<!--    lombok 插件    -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--        测试工具 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
<!--      spring security 依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--JSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>

4.配置文件

spring:
  #数据库链接配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/zm?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
# mybatis配置
mybatis:
  # xml文件扫描路径
  mapper-locations: classpath:mapper/**/*.xml
  # 扫描实体
  type-aliases-package: com.zm.test.entity
  # 开启驼峰模式
  configuration:
    map-underscore-to-camel-case: true
# 日志配置
logging:
  config: classpath:log/logback-spring.xml

数据库设计

借用原博主的图片了,我的表设计可能和他字段有些出入,但是逻辑都是一样的

数据库设计

 建表语句:

CREATE TABLE `sys_user`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `accound` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '登录账户名',
  `nick_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户昵称',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
  `state` int(0) NULL DEFAULT 1 COMMENT '账户状态(1:正常,2:锁定)',
  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
  `birthday` datetime(0) NULL DEFAULT NULL COMMENT '生日',
  `phone` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '电话号',
  `sex` int(0) NULL DEFAULT NULL COMMENT '性别(0:女,1:男)',
  `avatar` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像地址',
  `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '创建人',
  `create_date` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '修改人',
  `update_date` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
  `delete_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '删除人',
  `delete_date` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
  `delete_flag` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除状态(0:未删除,1:已删除)',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `accound`(`accound`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
CREATE TABLE `sys_role`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `role_code` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色编码',
  `role_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名称',
  `role_desc` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
CREATE TABLE `sys_user_role`  (
  `uid` bigint(0) NOT NULL COMMENT '用户编号',
  `rid` bigint(0) NOT NULL COMMENT '角色编号',
  PRIMARY KEY (`uid`, `rid`) USING BTREE,
  INDEX `FK_Reference_10`(`rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
CREATE TABLE `sys_permission`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `permission_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限code',
  `permission_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限名',
  PRIMARY KEY (`id`) USING BTREE
)
CREATE TABLE `sys_request_path`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `url` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求路径',
  `description` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路径描述',
  PRIMARY KEY (`id`) USING BTREE
)
CREATE TABLE `sys_role_permission`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `role_id` bigint(0) NULL DEFAULT NULL COMMENT '角色id',
  `permission_id` bigint(0) NULL DEFAULT NULL COMMENT '权限id',
  PRIMARY KEY (`id`) USING BTREE
)
CREATE TABLE `sys_request_path_permission`  (
  `id` bigint(0) NULL DEFAULT NULL COMMENT '主键id',
  `url_id` bigint(0) NULL DEFAULT NULL COMMENT '请求路径id',
  `permission_id` bigint(0) NULL DEFAULT NULL COMMENT '权限id'
)

表数据语句:

//sys_user
INSERT INTO `sys_user`(`id`, `accound`, `nick_name`, `password`, `state`, `email`, `birthday`, `phone`, `sex`, `avatar`, `create_by`, `create_date`, `update_by`, `update_date`, `delete_by`, `delete_date`, `delete_flag`, `remark`) VALUES (1, 'user1', '用户1', '$2a$10$47lsFAUlWixWG17Ca3M/r.EPJVIb7Tv26ZaxhzqN65nXVcAhHQM4i', 1, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'user1', '2022-04-29 01:19:21', NULL, NULL, '0', NULL);
INSERT INTO `sys_user`(`id`, `accound`, `nick_name`, `password`, `state`, `email`, `birthday`, `phone`, `sex`, `avatar`, `create_by`, `create_date`, `update_by`, `update_date`, `delete_by`, `delete_date`, `delete_flag`, `remark`) VALUES (2, 'user2', '用户2', '$2a$10$uSLAeON6HWrPbPCtyqPRj.hvZfeM.tiVDZm24/gRqm4opVze1cVvC', 1, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'user2', '2022-04-25 07:18:29', NULL, NULL, '0', NULL);
INSERT INTO `sys_user`(`id`, `accound`, `nick_name`, `password`, `state`, `email`, `birthday`, `phone`, `sex`, `avatar`, `create_by`, `create_date`, `update_by`, `update_date`, `delete_by`, `delete_date`, `delete_flag`, `remark`) VALUES (3, 'user3', '用户2', '$2a$10$uSLAeON6HWrPbPCtyqPRj.hvZfeM.tiVDZm24/gRqm4opVze1cVvC', 1, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '0', NULL);


//sys_role
INSERT INTO `sys_role`(`id`, `role_code`, `role_name`, `role_desc`) VALUES (1, 'admin', '管理员', '管理员,拥有所有权限');
INSERT INTO `sys_role`(`id`, `role_code`, `role_name`, `role_desc`) VALUES (2, 'user', '普通用户', '普通用户,拥有部分权限');

//sys_user_role
INSERT INTO `sys_user_role`(`uid`, `rid`) VALUES (1, 1);
INSERT INTO `sys_user_role`(`uid`, `rid`) VALUES (2, 2);

//sys_permission
INSERT INTO `sys_permission`(`id`, `permission_code`, `permission_name`) VALUES (1, 'create_user', '创建用户');
INSERT INTO `sys_permission`(`id`, `permission_code`, `permission_name`) VALUES (2, 'query_user', '查看用户');
INSERT INTO `sys_permission`(`id`, `permission_code`, `permission_name`) VALUES (3, 'delete_user', '删除用户');
INSERT INTO `sys_permission`(`id`, `permission_code`, `permission_name`) VALUES (4, 'modify_user', '修改用户');

//sys_request_path
INSERT INTO `sys_request_path`(`id`, `url`, `description`) VALUES (1, '/sysUser/getUser', '查询用户');

//sys_role_permission
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`) VALUES (1, 1, 1);
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`) VALUES (2, 1, 2);
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`) VALUES (3, 1, 3);
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`) VALUES (4, 1, 4);
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`) VALUES (5, 2, 1);

//sys_request_path_permission
INSERT INTO `sys_request_path_permission`(`id`, `url_id`, `permission_id`) VALUES (1, 1, 2);

Spring Security核心配置:WebSecurityConfig

创建WebSecurityConfig继承WebSecurityConfigurerAdapter类,并实现configure(AuthenticationManagerBuilder auth)和 configure(HttpSecurity http)方法。后续我们会在里面加入一系列配置,包括配置认证方式、登入登出、异常处理、会话管理等。

@Configuration
@EnableWebSecurity  //开启Spring Security的功能
//prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
    }
}

用户登录认证逻辑UserDetailsService

1.自定义实现类

为了添加自定义认证信息,我们需要实现security的UserDetails实现类,这里我选择继承security默认实现的User类。

@EqualsAndHashCode(callSuper = false)
public class SecuritySysUser extends User {

    /**
     * 用户信息
     */
    @Setter
    @Getter
    private SysUser sysuser;
    /**
     * 权限信息
     */
    @Setter
    @Getter
    private List<SysRole> roleList;

    /**
     * 构造方法
     * @param username
     * @param password
     * @param authorities  用户权限列表
     */
    public SecuritySysUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, true, true, true, true, authorities);
    }
}

2.创建自定义UserDetailsService

这是实现自定义用户认证的核心逻辑,loadUserByUsername(String username)的参数就是登录时提交的用户名,返回类型是一个叫UserDetails 的接口,这里我们使用自定义的实体SecuritySysUser实现构造方法。

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysPermissionService sysPermissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if(StringUtils.isEmpty(username)){
            throw new UsernameNotFoundException("用户名不能为空");
        }
        /**
         * 通过用户名称获取用户信息
         */
        SysUser user=sysUserService.getUserDetails(username);
        
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        //获取该用户所拥有的权限
        List<SysPermission> sysPermissions = sysPermissionService.selectListPermissionByUser(user.getId());
        // 声明用户授权
        sysPermissions.forEach(sysPermission -> {
            GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(sysPermission.getPermissionCode());
            grantedAuthorities.add(grantedAuthority);
        });
        SecuritySysUser ssu=new SecuritySysUser(user.getAccound(),user.getPassword(),grantedAuthorities);
        ssu.setSysuser(user);
        return ssu;
    }
}

其中User实体默认参数解释

String username:用户名
String password: 密码
boolean enabled: 账号是否可用
boolean accountNonExpired:账号是否过期
boolean credentialsNonExpired:密码是否过期
boolean accountNonLocked:账号是否锁定
Collection<? extends GrantedAuthority> authorities):用户权限列表

3.service和dao层方法

1.根据用户名查询用户信息

service层    
/**
     * 根据登录名获取用户信息
     * @param username
     */
    SysUser getUserDetails(String username);



dao层
    <!-- 根据登录名获取用户信息 -->
    <select id="getUserDetails" resultType="com.zm.test.entity.zm.SysUser">
        select * from sys_user where accound=#{username}
    </select>

2.根据用户id查询权限信息

service    
/**
     * 根据用户id查询权限信息
     * @param userId
     * @return
     */
    List<SysPermission> selectListPermissionByUser(Long userId);



dao层
    <select id="selectListPermissionByUser" resultType="com.zm.test.entity.zm.SysPermission">
        SELECT
        p.*
        FROM
        sys_user AS u
        LEFT JOIN sys_user_role AS ur ON u.id = ur.uid
        LEFT JOIN sys_role AS r ON r.id = ur.rid
        LEFT JOIN sys_role_permission AS rp ON r.id = rp.role_id
        LEFT JOIN sys_permission AS p ON p.id = rp.permission_id
        WHERE u.id = #{userId}
    </select>

然后将我们的自定义的基于JDBC的用户认证在之前创建的WebSecurityConfig 中得configure(AuthenticationManagerBuilder auth)中声明一下,到此自定义的基于JDBC的用户认证就完成了

/**
     * 自定义用户登录操作
     */
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

用户名密码加密

新版本的Spring security规定必须设置一个默认的加密方式,不允许使用明文。这个加密方式是用于在登录时验证密码、注册时需要用到。
    我们可以自己选择一种加密方式,Spring security为我们提供了多种加密方式,我们这里使用一种强hash方式进行加密。

加密方式.png

在WebSecurityConfig 中注入(注入即可,不用声明使用),这样就会对提交的密码进行加密处理了,如果你没有注入加密方式,运行的时候会报错"There is no PasswordEncoder mapped for the id"错误。

/**
 * 指定加密方式
 * @return
 */
@Bean
public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

修改configure(AuthenticationManagerBuilder auth)方法

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService)
    .passwordEncoder(passwordEncoder());
}

同样的我们数据库里存储的密码也要用同样的加密方式存储,例如我们将123456用BCryptPasswordEncoder 加密后存储到数据库中(注意:即使是同一个明文用这种加密方式加密出来的密文也是不同的,这就是这种加密方式的特点)

/**
 * 添加用户
 * @param user
 */
@Override
public void addUser(SysUser user) {
    // 加密密码
    user.setPassword(passwordEncoder.encode(user.getPassword()));
    sysUserMapper.addUser(user);
}

 到此处,我们的权限认证就可以执行了,只不过这个并不是前后端分离的。

屏蔽Spring Security默认重定向登录页面以实现前后端分离功能

在前后端分离的情况下(比如前台使用VUE或JQ等)我们需要的是在前台接收到"用户未登录"的提示信息,所以我们接下来要做的就是屏蔽重定向的登录页面,并返回统一的json格式的返回体。而实现这一功能的核心就是实现AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)

登录失败处理

/**
 * @Author: zm
 * @Description:匿名用户访问无权限资源时的异常
 * @Date: 2022/4/24 10:10
 */
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

在WebSecurityConfig中的configure(HttpSecurity http)方法中声明

//异常处理(权限拒绝、登录失效等)

.and().exceptionHandling()

. authenticationEntryPoint(authenticationEntryPoint).//匿名用户访问无权限资源时的异常处理

实现登录成功/失败、登出处理逻辑

首先需要明白一件事,对于登入登出我们都不需要自己编写controller接口,Spring Security为我们封装好了。默认登入路径:/login,登出路径:/logout。当然我们可以也修改默认的名字。登录成功失败和登出的后续处理逻辑如何编写会在后面慢慢解释。
    当登录成功或登录失败都需要返回统一的json返回体给前台,前台才能知道对应的做什么处理。
而实现登录成功和失败的异常处理需要分别实现AuthenticationSuccessHandler和AuthenticationFailureHandler接口并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中然后声明

登录成功:

/**
 * @Author: zm
 * @Description:登录成功处理逻辑
 * @Date: 2022/4/24 10:18
 */
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private SysUserService sysUserService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //更新用户表上次登录时间、更新人、更新时间等字段
        User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        SysUser sysUser = sysUserService.getUserDetails(userDetails.getUsername());
//        sysUser.setLastLoginTime(new Date());
        sysUser.setUpdateDate(LocalDateTime.now());
        sysUser.setUpdateBy(sysUser.getAccound());
        sysUserService.update(sysUser);

        //此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
        //进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展

        //返回json数据
        JsonResult result = ResultTool.success();
        //处理编码方式,防止中文乱码的情况
        httpServletResponse.setContentType("text/json;charset=utf-8");
        //塞到HttpServletResponse中返回给前台
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

登录失败:

登录失败处理器主要用来对登录失败的场景(密码错误、账号锁定等…)做统一处理并返回给前台统一的json返回体。还记得我们创建用户表的时候创建了账号过期、密码过期、账号锁定之类的字段吗,这里就可以派上用场了.

/**
 * @Author: zm
 * @Description:登录失败处理逻辑
 * @Date: 2022/4/24 10:39
 */
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //返回json数据
        JsonResult result = null;
        if (e instanceof AccountExpiredException) {
            //账号过期
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
        } else if (e instanceof BadCredentialsException) {
            //密码错误
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
        } else if (e instanceof CredentialsExpiredException) {
            //密码过期
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
        } else if (e instanceof DisabledException) {
            //账号不可用
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
        } else if (e instanceof LockedException) {
            //账号锁定
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
        } else if (e instanceof InternalAuthenticationServiceException) {
            //用户不存在
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
        }else{
            //其他错误
            result = ResultTool.fail(ResultCode.COMMON_FAIL);
        }
        //处理编码方式,防止中文乱码的情况
        httpServletResponse.setContentType("text/json;charset=utf-8");
        //塞到HttpServletResponse中返回给前台
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

登出:

/**
 * @Author: Administrator
 * @Description: 登出
 * @Date: 2022/4/24 10:46
 */
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {


    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        JsonResult result = ResultTool.success();
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

在WebSecurityConfig中的configure(HttpSecurity http)方法中声明

//登入
.and().formLogin().
permitAll().//允许所有用户
successHandler(authenticationSuccessHandler).//登录成功处理逻辑
failureHandler(authenticationFailureHandler).//登录失败处理逻辑
//登出
and().logout().
permitAll().//允许所有用户
logoutSuccessHandler(logoutSuccessHandler).//登出成功处理逻辑
deleteCookies("JSESSIONID")//登出之后删除cookie

会话管理(登录过时、限制单用户或多用户登录等)

限制登录用户数量

and().sessionManagement(). maximumSessions(1)

处理账号被挤下线处理逻辑

当账号异地登录导致被挤下线时也要返回给前端json格式的数据,比如提示"账号下线"、"您的账号在异地登录,是否是您自己操作"或者"您的账号在异地登录,可能由于密码泄露,建议修改密码"等。这时就要实现SessionInformationExpiredStrategy(会话信息过期策略)来自定义会话过期时的处理逻辑。

@Component
public class CustomizeSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        JsonResult result = ResultTool.fail(ResultCode.USER_ACCOUNT_USE_BY_OTHERS);
        HttpServletResponse httpServletResponse = event.getResponse();
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

在WebSecurityConfig中声明

然后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后声明,在配置同时登录用户数的配置下面再加一行 expiredSessionStrategy(sessionInformationExpiredStrategy)
 

.and()
                .sessionManagement()
                //设置一个账号只能一个用户使用
                .maximumSessions(1)
                //会话信息过期策略会话信息过期策略(账号被挤下线)
                .expiredSessionStrategy(sessionInformationExpiredStrategy)

实现基于JDBC的动态权限控制

其实我们就已经实现了一个所谓的基于RBAC的权限控制,只不过我们是在WebSecurityConfig中写死的,但是在平时开发中,难道我们每增加一个需要访问权限控制的资源我们都要修改一下WebSecurityConfig增加一个antMatchers(…)吗,肯定是不合理的。因此我们现在要做的就是将需要权限控制的资源配到数据库中,当然也可以存储在其他地方,比如用一个枚举,只是我觉得存在数据库中更加灵活一点。
    我们需要实现一个AccessDecisionManager(访问决策管理器),在里面我们对当前请求的资源进行权限判断,判断当前登录用户是否拥有该权限,如果有就放行,如果没有就抛出一个"权限不足"的异常。不过在实现AccessDecisionManager之前我们还需要做一件事,那就是拦截到当前的请求,并根据请求路径从数据库中查出当前资源路径需要哪些权限才能访问,然后将查出的需要的权限列表交给AccessDecisionManager去处理后续逻辑。那就是需要先实现一个SecurityMetadataSource,翻译过来是"安全元数据源",我们这里使用他的一个子类FilterInvocationSecurityMetadataSource。
    在自定义的SecurityMetadataSource编写好之后,我们还要编写一个拦截器,增加到Spring security默认的拦截器链中,以达到拦截的目的。
    同样的最后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后声明

权限拦截器

/**
 * @Author: zm
 * @Description:权限拦截器
 * @Date: 2022/4/24 11:47
 */
@Service
public class CustomizeAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(CustomizeAccessDecisionManager accessDecisionManager) {
        super.setAccessDecisionManager(accessDecisionManager);
    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        invoke(fi);
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        //fi里面有一个被拦截的url
        //里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
        //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            //执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }
}

安全元数据源FilterInvocationSecurityMetadataSource

/**
 * @Author: zm
 * @Description:安全元数据源
 * @Date: 2022/4/24 11:52
 */
@Component
public class CustomizeFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Autowired
    SysPermissionService sysPermissionService;
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //获取请求地址
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        requestUrl=requestUrl.substring(0, requestUrl.indexOf("?"));
        //查询具体某个接口的权限
        List<SysPermission> permissionList =  sysPermissionService.selectListByPath(requestUrl);
        if(permissionList == null || permissionList.size() == 0){
            //请求路径没有配置权限,表明该请求接口可以任意访问
            return null;
        }
        String[] attributes = new String[permissionList.size()];
        for(int i = 0;i<permissionList.size();i++){
            attributes[i] = permissionList.get(i).getPermissionCode();
        }
        return SecurityConfig.createList(attributes);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

访问决策管理器AccessDecisionManager

/**
 * @Author: zm
 * @Description:访问决策管理器
 * @Date: 2022/4/24 11:56
 */
@Component
public class CustomizeAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        Iterator<ConfigAttribute> iterator = collection.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute ca = iterator.next();
            //当前请求需要的权限
            String needRole = ca.getAttribute();
            //当前用户所具有的权限
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足!");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

在WebSecurityConfig中声明

先在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后声明

http.authorizeRequests().
                withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
                        o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
                        return o;
                    }
                });
        http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);//增加到默认拦截链中

最终的WebSecurityConfig配置

/**
 * spring security 配置类
 * @Author: zm
 * @Description:
 * @Date: 2022/4/22 13:48
 */
@Configuration
@EnableWebSecurity  //开启Spring Security的功能
//prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 自定义用户登录操作
     */
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    /**
     * 匿名用户访问无权限资源时的异常
     */
    @Autowired
    private CustomizeAuthenticationEntryPoint authenticationEntryPoint;
    /**
     * 登录成功执行方法
     */
    @Autowired
    private CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;
    /**
     * 登陆失败执行方法
     */
    @Autowired
    private CustomizeAuthenticationFailureHandler authenticationFailureHandler;
    /**
     * 登出成功执行方法
     */
    @Autowired
    private CustomizeLogoutSuccessHandler logoutSuccessHandler;

    /**
     * 会话过期策略处理
     */
    @Autowired
    private CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;


    //自定义权限访问设置 =======开始=========
    /**
     * 访问决策管理器
     */
    @Autowired
    private CustomizeAccessDecisionManager accessDecisionManager;
    /**
     * 安全元数据源
     */
    @Autowired
    private CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;
    /**
     * 权限拦截器
     */
    @Autowired
    private CustomizeAbstractSecurityInterceptor securityInterceptor;
    //自定义权限访问设置 =======结束=========

    /**
     * 指定加密方式
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
        .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();
        http
                .authorizeRequests()
                //自定义权限控制器
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
                        o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
                        return o;
                    }
                })
                .antMatchers(HttpMethod.POST, "/sysUser/addUser").permitAll() // 允许post请求/add-user,而无需认证
                .anyRequest().authenticated() //   有请求都需要验证
                //登入
                .and().formLogin().
                permitAll().//允许所有用户
                successHandler(authenticationSuccessHandler).//登录成功处理逻辑
                failureHandler(authenticationFailureHandler).//登录失败处理逻辑
                //登出
                and().logout().
                permitAll().//允许所有用户
                logoutSuccessHandler(logoutSuccessHandler).//登出成功处理逻辑
                deleteCookies("JSESSIONID")//登出之后删除cookie
                //异常处理(权限拒绝、登录失效等)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
                .and()
                .sessionManagement()
                // 无状态session,不进行存储
//                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                //设置一个账号只能一个用户使用
                .maximumSessions(1)
                //会话信息过期策略会话信息过期策略(账号被挤下线)
                .expiredSessionStrategy(sessionInformationExpiredStrategy)

        ;
        http.addFilterBefore(securityInterceptor,FilterSecurityInterceptor.class);
    }
}

至此为止本文就基本结束了,在本文中我们利用Springboot+Spring security实现了前后端分离的用户登录认证和动态的权限访问控制。

如有疑问可以联系我,最后还是要谢谢这篇博客的博主

Springboot + Spring Security 实现前后端分离登录认证及权限控制_I_am_Rick_Hu的博客-CSDN博客_springsecurity前后端分离登录认证

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值