上一章中使用的是默认的安全机制,仅有一个用户,一种角色。实际开发中,我们有很多用户,很多的角色,本章来实现多用户授权。

一、资源准备

假设我们有2中角色,一种是管理员,有后台操作权限,一种是普通用户,有网站的访问权限。

1 新建controller

新建2个controller,AdminController和UserController。AdminController中是后台管理员的相关操作。UserController中是用户登录后才能进行的操作,比如修改密码,头像等。

springsecurity 授权流程 springsecurity按钮授权_java

2 资源授权

springsecurity 授权流程 springsecurity按钮授权_spring_02

antMatchers()采用ANT模式的URL匹配器。ANT模式使用?匹配任意单个字符,使用*匹配0或任意数量的字符,使用**匹配0或者更多的目录。/admin/**表示admin下的所有请求。hasRole(“ADMIN”)表示ADMIN角色才能访问。

3 重启服务

重启服务,登录后,发现/user/hello 和/admin/hello都无法访问。

配置中给用户添加spring.security.user.roles = USER后,/user/hello可以访问

二、基于内存的多用户支持

1 修改配置文件

我们先将配置中的用户删除,我习惯使用yml的方式,所以文件改为application-dev.yml

springsecurity 授权流程 springsecurity按钮授权_spring_03


我们用-dev来区分不通环境,所以在启动项中添加环境配置:

springsecurity 授权流程 springsecurity按钮授权_springsecurity 授权流程_04

第一步点击下拉箭头选择编辑,然后添加参数:-Dspring.profiles.active=dev。项目启动就会读取指定配置。

2 实现一个自定义的UserDetailsService

在配置类中:

@Bean
    public UserDetailsService userDetailService(){
        InMemoryUserDetailsManager memory = new InMemoryUserDetailsManager();
        memory.createUser(User.withUsername("user").password("123").roles("USER").build());
        memory.createUser(User.withUsername("admin").password("123").roles("ADMIN").build());
        return memory;
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.contentEquals(rawPassword);
            }
        };
    }

在配置类中自定义UserDetailsService来代替默认的。并添加2个用户。

添加自定义的PasswordEncoder来校验密码。

3 启动项目验证

启动后,user用户登录只能才能访问user/hello。admin用户登录才能访问admin/hello。

二、基于mysql的多用户支持

自定义数据库结构实际上也仅需实现一个自定义的UserDetailsService,让spring发现注入,然后会自动为我们完成认证和授权。

UserDetailsService仅定义了一个loadUserByUsername方法,用于获取一个UserDetails对象。
UserDetails对象包含了一系列在验证时会用到的信息,包括用户名、密码、权限以及其他信息,SpringSecurity会根据这些信息判定验证是否成功。

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

也就是说,不管你采用什么数据源,只要能实现UserDetailsService,并且返回UserDetails就行。

1. 数据库准备

sql:

CREATE TABLE `users` (
    `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `username` VARCHAR(50) NOT NULL UNIQUE DEFAULT '' COMMENT '用户名',
    `password` varchar(255) NOT NULL DEFAULT '' COMMENT '密码',
    `role` TINYINT(5) unsigned NOT NULL DEFAULT '0' COMMENT '用户类型 0普通用户,1管理员',
    `delete` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除,0否,1是',
    `createtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updatetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    KEY `idx_createtime`(`createtime`),
    KEY `idx_username`(`username`)
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='用户表';

添加连接配置:

spring:
    datasource:
        url: jdbc:mysql://localhost:3306/novel?serverTimezone=GMT%2B8&autoReconnect=true&useSSL=false&failOverReadOnly=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
2.编码实现UserDetails
@Data
public class BaseDO {

    private Long id;

    private String createTime;

    private String updateTime;
}

@Data
@Table(name = "users")
public class UsersDO extends BaseDO implements UserDetails {
    private String username;

    private String password;

    private Integer role;

    private Boolean enable;

    @Column(exist = false) //表示不是数据库字段--自定义注解
    private Set<GrantedAuthority> authorities;

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

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

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

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

    @Override
    public boolean isEnabled() {
        return this.enable;
    }
}

实现UserDetails定义的几个方法:

  • isAccountNonExpired、isAccountNonLocked 和 isCredentialsNonExpired 暂且用不到,统一返回true
  • isEnabled对应isDelete字段
  • getAuthorities方法本身对应的是roles字段,但由于结构不一致,所以此处新建一个,并在后续进行填充。
3.关于数据库查询方式

springsecurity 授权流程 springsecurity按钮授权_springsecurity 授权流程_05


封装Spring的JdbcTemplate,自己写的查询方式。代码gitee中,文章开头给出了。

4. 创建UserDetailsService
@Repository
public class UserDao {
    @Resource
    private EasyJdbc easyJdbc;

    public UsersDO findUserByUserName(String username){
        String sql = "select * from users where username = :username limit 1";
        Map<String, String> param = new HashMap<>();
        param.put("username", username);
        return easyJdbc.queryForObject(sql, param, UsersDO.class);
    }
}

@Service
public class UserServiceImpl implements UserDetailsService {
    @Resource
    private UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UsersDO userDD = userDao.findUserByUserName(username);
        if (userDD == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        userDD.setAuthorities(AuthorityUtil.generateAuthority(userDD.getRole()));
        return userDD;
    }
}

service中查询出UserDO之后,需要给authorities赋值。自己实现AuthorityUtil类,将role转为GrantedAuthority对象。

public class AuthorityUtil {

    public static Set<GrantedAuthority> generateAuthority(Integer role){
        Set<GrantedAuthority> set = new HashSet<>();
        set.add(new SimpleGrantedAuthority(RoleEnum.getAuthority(role)));
        return set;
    }
}

@Getter
@AllArgsConstructor
public enum RoleEnum {
    /**
     * user
     */
    USER(0, "ROLE_USER"),
    ADMIN(1, "ROLE_ADMIN"),
    ;

    private final int role;

    private final String authority;

    public static String getAuthority(Integer role){
        for (RoleEnum roleEnum : RoleEnum.values()){
            if (roleEnum.role == role){
                return roleEnum.authority;
            }
        }
        return null;
    }
}

对应的角色需要以ROLE_开头的原因是配置类中hasRole方法中判断了必须以ROLE_开头。

.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")

如果不想用ROLE_开头,可以选择替换为调用hasAuthority()方法。

三、总结

1.讲了基于内存和基于数据库实现多用户授权和认证。不管什么方式,最重要的是实现UserDetailsService,返回UserDetails。剩下的交给SpringSecurity来处理。

2.本文查数据库使用的是自己封装的Spring的JdbcTemplate,有兴趣可以再文章开头gitee链接中查看

3.本文新建了不少目录:

springsecurity 授权流程 springsecurity按钮授权_安全_06