SpringBoot集成SpringSecurity

127 阅读6分钟

最近看了一下SpringSecurity方面的东西,之前都是接手项目,权限什么的都做好了,现在有点时间正好看一下,顺带做个最简单的Demo,进行一个初步的学习。

1、环境搭建(gradle版本是6.8)

plugins {
    id 'org.springframework.boot' version '2.6.6'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    maven { url "https://maven.aliyun.com/nexus/content/groups/public"}
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.baomidou:mybatis-plus-boot-starter:3.2.0'
    runtimeOnly 'mysql:mysql-connector-java:8.0.25'
    testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.2.0'
}

2、项目结构:

image.png

3、核心配置

3.1、UserService:主要实现SpringSecurity的UserDetailsService

package wyf.application.service.impl;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import wyf.domain.SecurityUserDetails;
import wyf.domain.User;
import wyf.repository.UserRepository;
import wyf.application.service.UserService;

import javax.annotation.Resource;
import java.util.Objects;
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findUserByName(username);
        if (Objects.nonNull(user)) {
            return new SecurityUserDetails(user);
        }
        return null;
    }
}

loadUserByUsername方法的返回值是UserDetails,所以需要一个类去实现SpringSecurity提供的UserDetails接口

3.2、SecurityUserDetails:实现UserDetails接口

package wyf.domain;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class SecurityUserDetails implements UserDetails {

    private User user;

    public SecurityUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.createAuthorityList(user.getRoles());
    }

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

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

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

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

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

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

3.3、User类:用户类,关键是用户角色设置有要求,必须以"ROLE_"开头

package wyf.domain;


import java.util.List;
import java.util.Objects;
public class User {
    private long id;
    private String username;
    private String password;
    private List<Role> roleList;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<Role> getRoleList() {
        return roleList;
    }

    public void setRoleList(List<Role> roleList) {
        this.roleList = roleList;
    }

    public String getRoles(){
        StringBuilder sb = new StringBuilder();
        if (Objects.nonNull(roleList)) {
            for (Role role : roleList) {
                sb.append("ROLE_").append(role.getName()).append(",");
            }
        }
        return sb.substring(0,sb.length() > 0 ? sb.length() - 1 : sb.length());
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + username + ''' +
                ", password='" + password + ''' +
                ", roleList=" + roleList +
                '}';
    }
}

至于为什么要以ROLE_开头,这是SpringSecurity的要求,否则检查用户角色时会报错,下面的配置类中有方法hasAnyRole方法或者用hasRole方法也许,源码里有这样一段代码检查用户拥有的角色,按它的检查原则,我们自己存的角色名要么在数据库里存成以ROLE开头的,要么自己组装一下放在内存,我选择后者。源码如下:

image.png 这个rolePrefix就是ROLE_在源码中如下:

image.png 我是这么理解的,因为我不加ROLE_前缀会报错403,然后点源码看到的,上面那些代码干了啥,我还没去看,之后再看,先把它用起来。

3.4、SecurityConfig:SpringSecurity配置类

package wyf.config;

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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import wyf.application.service.UserService;

import javax.annotation.Resource;

@Configuration
@EnableWebSecurity//用于启用Web安全的注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserService userService;

    // 进行认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    // 配置拦截
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/user/getUser")
                .hasAnyRole("admin")
                .antMatchers("/user/login")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

以上是SpringSecurity的核心配置类,UserService是一个继承了SpringSecurity中UserDetailsService接口的接口,它的实现类主要是从数据库查询出号密码以及相关角色、权限等进行比对。PasswordEncoder是SpringSecurity对密码的加密解密bean。配置拦截主要是说"/user/login"路径允许访问,"/user/getUser"需要admin角色才能访问,formLogin表示使用SpringSecurity默认的登录页面。

4、数据库配置:

4.1、t_user表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
  `phone_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
  `enable` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '是否可用:0-可用,1-不可用',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  `update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '更新人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, '张三', '$2a$10$x0OunSegxpe5/9/YS0XNauvrHWFnnT0kwkv/Xb9e3FrH0Lmz6rx/e', '15235084160', '0', '2022-04-05 22:31:03', 'admin', '2022-04-05 22:31:11', 'admin');
INSERT INTO `t_user` VALUES (2, '李四', '$2a$10$hYG4KPj6ypeY.QMfndlEqeXMN1YM86dskMXBrKluxKtHZz3Y1.U5K', '11111111111', '0', '2022-04-07 14:28:08', 'admin', '2022-04-07 14:28:14', 'admin');
SET FOREIGN_KEY_CHECKS = 1;

4.2、t_role表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名',
  `enable` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '是否可用:0-可用,1-不可用',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  `update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '更新人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_role
-- ----------------------------
INSERT INTO `t_role` VALUES (1, 'admin', '0', '2022-04-07 11:12:42', 'admin', '2022-04-07 11:12:49', 'admin');
INSERT INTO `t_role` VALUES (2, 'user', '0', '2022-04-07 14:28:48', 'admin', '2022-04-07 14:28:55', 'admin');
SET FOREIGN_KEY_CHECKS = 1;

4.3、t_user_role表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint NOT NULL COMMENT '用户ID,t_user表的主键ID',
  `user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  `role_id` bigint NOT NULL COMMENT '角色ID,t_role表的主键ID',
  `role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名',
  `enable` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '是否可用:0-可用,1-不可用',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  `update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '更新人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user_role
-- ----------------------------
INSERT INTO `t_user_role` VALUES (1, 1, '张三', 1, 'admin', '0', '2022-04-07 11:11:21', 'admin', '2022-04-07 11:11:28', 'admin');
INSERT INTO `t_user_role` VALUES (2, 2, '李四', 2, 'user', '0', '2022-04-07 14:29:26', 'admin', '2022-04-07 14:29:32', 'admin');
SET FOREIGN_KEY_CHECKS = 1;

5、运行结果:

启动项目,浏览器输入http://127.0.0.1:8080/user/getUser?username=李四 回车。然后到达SpringSecurity提供的登陆页面。输入用户名李四,密码123321,点击登录,由于李四没有admin角色,所以会403,如图所示: image.png 然后再次重启项目使用张三进行登录,浏览器输入http://127.0.0.1:8080/user/getUser?username=张三 回车,进行登录,用户名张三,密码123456如图,成功。 image.png 输入的明文密码SpringSecurity通过PasswordEncoder这个bean会对密码进行加密,然后和从数据库中查询的密码进行比对,一般的做法是在用户注册的时候,将密码进行加密,存储到数据库,我这没有注册,所以先把密码进行加密,直接存在了数据库。代码位于test包下,想给什么密码进行加密就替换一下"123321",如图输出的就是加密后的密码:

package mappertest;

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class TestFindUserByUserName {

    @Test
    public void getEncodePassword(){
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode("123321");
        System.out.println(encode);
    }

}

6、一个问题:

使用PasswordEncoder对密码进行加密解密的时候,我一开始在数据库里存储的就是明文密码,想在UserRepositoryImpl里先把密码查出来,再进行加密,然后设置回内存中的user对象,于是在UserRepositoryImpl里引入了PasswordEncoder的bean,结果报错了,循环依赖,如图。然后我乖乖在数据库里存储了加密后的密码。 image.png

7、GITHUB地址:SpringBoot集成SpringSecurity