技术栈:
SpringBoot: 2.1.9
Mybatis Plus: 3.3.0
概述
接上一篇文章,之前给出的用户名和密码的信息都是在配置文件里固定写死的,这种做法并不适合实际的开发,在实际的项目系统中,用户名和密码都应该交由数据库进行管理。
用户登录时,系统会根据用户名,从存储设备查找该用户的密码及权限等,将其组装成一个UserDetails对象。并用UserDetails中的数据对用户进行认证,决定其输入的用户名/密码是否正确。
下面介绍这部分最重要的两个接口:UserDetailsService和UserDetails。
一、新增一个自定义的类来实现UserDetailsService接口,并注入我们自己的service。
UserDetailsService用来加载用户的信息,并没有其他功能。
默认实现:
UserDetailsService => UserDetailsManager => InMemoryUserDetailsManager => 存储于内存
UserDetailsService => JdbcDaoImpl => 存储于数据库(磁盘)
/**
* @program sweet-dream
* @description:
* @author: zhangchao
* @date: 2020/03/15 15:41
* @since: 1.0.0
*/
@Slf4j
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private IGyUserService userService;
@Autowired
private ISysRoleService roleService;
@Autowired
private ISysUserRoleService userRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("对用户 [{}] 进行信息加载...",username);
GyUser user = (GyUser) userService.getOne(new QueryWrapper<GyUser>().lambda().eq(GyUser::getUserCode,username)).getData();
if(user==null){
log.info("用户 [{}] 未找到",username);
throw new UsernameNotFoundException("Username:["+username+"] not found");
}
log.info("user:" + user);
List<SysRole> rolesList = new ArrayList<>();
List<SysUserRole> userRoleList = (List<SysUserRole>) userRoleService.list(new QueryWrapper<SysUserRole>().lambda().eq(SysUserRole::getUserAccount,username)).getData();
if (isNotEmpty(userRoleList)){
userRoleList.forEach(sysUserRole -> {
rolesList.add((SysRole) roleService.getOne(new QueryWrapper<SysRole>().lambda().eq(SysRole::getCode,sysUserRole.getRoleCode())).getData());
});
}
//设置权限
user.setUserRoles(CollectionUtil.removeNull(rolesList));
log.info("用户 [{}] 信息加载完成",username);
// SecurityUser 实现UserDetails
return new SecurityUser(user);
}
//
// public static void main(String[] args) {
// System.out.println(new BCryptPasswordEncoder().encode("666666"));
// }
}
UserDetailsService接口只提供了一个方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
该方法很容易理解:通过用户名来加载用户 。这个方法主要用于从系统数据中查询并加载具体的用户到Spring Security中。
CustomUserDetailsService类实现了UserDetailsService接口,里面有一个loadUserByUsername(String s)方法,这个方法返回一个UserDetails ,UserDetails 是一个接口,而org.springframework.security.core.userdetails.User实现了UserDetails,因此这里我们可以直接使用SpringSecurity提供的User对象,当然如果不想使用SpringSecurity提供的User对象,我们也可以自己编写一个实现UserDetails接口的对象。
二、新增SecurityUser实现UserDetails接口
* @program sweet-dream
* @description:
* @author: zhangchao
* @date: 2020/03/15 14:55
* @since: 1.0.0
*/
@Slf4j
public class SecurityUser implements UserDetails {
private String uuid;
private String userCode;
private String username;
private String password;
private String sex;
private List<SysRole> roles;
public SecurityUser(GyUser user) {
if (null != user){
this.uuid = user.getUuid();
this.userCode = user.getUserCode();
this.username = user.getUserCode();
this.password = user.getPassWord();
this.sex = user.getSex();
this.roles = user.getUserRoles();
}
}
/**
* 权限集合
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
List<SysRole> roles = this.getRoles();
if(roles != null){
for (SysRole role : roles) {
System.out.println("role: "+role);
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+role.getName());
authorities.add(authority);
}
}
log.info("获取登录用户已具有的权限:{}", authorities.toString());
return authorities;
}
/**
* 指示用户的账户是否已过期。无法验证过期的账户。
* @return 如果用户的账户有效(即未过期),则返回true,如果不在有效就返回false
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指示用户是锁定还是解锁。无法对锁定的用户进行身份验证。
* @return 如果用户未被锁定,则返回true,否则返回false
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示用户的凭证(密码)是否已过期。过期的凭证阻止身份验证
* @return 如果用户的凭证有效(即未过期),则返回true
* 如果不在有效(即过期),则返回false
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 指示用户是启用还是禁用。无法对禁用的用户进行身份验证
* @return 如果启用了用户,则返回true,否则返回false
*/
@Override
public boolean isEnabled() {
return true;
}
/*********************set\get\toString方法***********************/
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getUserCode() {
return userCode;
}
public void setUserCode(String userCode) {
this.userCode = userCode;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public List<SysRole> getRoles() {
return roles;
}
public void setRoles(List<SysRole> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "SecurityUser{" +
"uuid='" + uuid + '\'' +
", userCode='" + userCode + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
", sex='" + sex + '\'' +
", roles=" + roles +
'}';
}
}
UserDetails
接口是我们自己用来定义用户表的结构的。
SpringSecurity自己的用户信息只包含了Username,password,roles,假如我希望用户的实体类中还有性别sex字段,那么就没有办法了,所以SpringSecurity提供了UserDetails接口,当然,UserDetails实例是通过UserDetailsService接口的loadUserByUsername
方法返回的。不过我们需要注意一下getAuthorities
方法,这个方法返回的是权限,但是我们返回的权限必须带有“ROLE_”开头才可以,SpringSecurity会自己截取ROLE_后边的字符串,也就是说,比如:我的权限叫ADMIN,那么,我返回告诉SpringSecurity的时候,必须告诉他权限是ROLE_ADMIN,这样SpringSecurity才会认为权限是ADMIN。
从上面UserDetailsService
可以知道最终交给Spring Security的是UserDetails
。该接口是提供用户信息的核心接口。该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象Authentication
中去。UserDetails
默认提供了:
- 用户的权限集, 默认需要添加
ROLE_
前缀 - 用户的加密后的密码, 不加密会使用
{noop}
前缀 - 应用内唯一的用户名
- 账户是否过期
- 账户是否锁定
- 凭证是否过期
- 用户是否可用
三、这我们的SecurityConfig中配置userDetailsService
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}
四、启动项目测试
输入用户名mayun,密码666666,登录成功!可以看到后台已经查询出此用户的所有权限。
PS 另外用到的一些其他类:
model类 GyUser
/**
* <p>
* 用户表设置
* </p>
*
* @author zhangchao
* @since 2020-03-02
*/
@Data
@TableName("Gy_User")
public class GyUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 唯一标识ID
*/
@TableId(value = "UUID",type = IdType.ASSIGN_UUID)
private String uuid;
/**
* 用户编码
*/
@TableField("UserCode")
private String UserCode;
/**
* 用户名称
*/
@TableField("UserName")
private String UserName;
/**
* 用户密码
*/
@TableField("PassWord")
private String PassWord;
/**
* 联系方式
*/
@TableField("Mobile")
private String Mobile;
/**
* 科室编码
*/
@TableField("DeptCode")
private String DeptCode;
/**
* HIS用户对应编码
*/
@TableField("HISUserCode")
private String HISUserCode;
/**
* 备注
*/
@TableField("Remark")
private String Remark;
/**
* 用户身份标识
*/
@TableField("IdentityFlag")
private Boolean IdentityFlag;
/**
* 创建时间
*/
@TableField("SysCreateDate")
private Date SysCreateDate;
/**
* 创建者
*/
@TableField("SysCreateBy")
private String SysCreateBy;
/**
* 更新时间
*/
@TableField("SysUpdateDate")
private Date SysUpdateDate;
/**
* 更新者
*/
@TableField("SysUpdateBy")
private String SysUpdateBy;
/**
* 版本号
*/
@TableField("VersionNum")
private Integer VersionNum;
/**
* 删除标识
*/
@TableField("SysDelFlag")
private String SysDelFlag;
/**
* 别名
*/
@TableField("alias")
private String alias;
/**
* 地址
*/
@TableField("address")
private String address;
/**
* 邮箱
*/
@TableField("email")
private String email;
/**
* 状态
*/
@TableField("status")
private String status;
/**
* 开始时间
*/
@TableField("startDate")
private Date startDate;
/**
* 结束时间
*/
@TableField("endDate")
private Date endDate;
/**
* 性别
*/
@TableField("sex")
private String sex;
/**
* 用户角色列表
*/
@TableField(exist = false)
private List<SysRole> userRoles;
}
service接口:IGyUserService
/**
* <p>
* 用户表设置 服务类
* </p>
*
* @author zhangchao
* @since 2020-03-02
*/
public interface IGyUserService extends BasicService<GyUser> {
}
service实现类:GyUserServiceImpl
/**
* <p>
* 用户表设置 服务实现类
* </p>
*
* @author zhangchao
* @since 2020-03-02
*/
@Service
public class GyUserServiceImpl extends BasicServiceImpl<GyUserMapper, GyUser> implements IGyUserService {
}
因为用到了mybatis plus,所以基础的CRUD接口就不用写了,mp都给你整合好了。
以上