上一章中使用的是默认的安全机制,仅有一个用户,一种角色。实际开发中,我们有很多用户,很多的角色,本章来实现多用户授权。
一、资源准备
假设我们有2中角色,一种是管理员,有后台操作权限,一种是普通用户,有网站的访问权限。
1 新建controller
新建2个controller,AdminController和UserController。AdminController中是后台管理员的相关操作。UserController中是用户登录后才能进行的操作,比如修改密码,头像等。
2 资源授权
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
我们用-dev来区分不通环境,所以在启动项中添加环境配置:
第一步点击下拉箭头选择编辑,然后添加参数:-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.关于数据库查询方式
封装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.本文新建了不少目录: