实际的业务系统中,用户与权限的对应关系通常是存放在RBAC权限模型的数据库表中的
UserDetailsService接口有一个方法叫做loadUserByUsername,我们实现动态加载用户、角色、权限信息就是通过实现该方法。函数见名知义:通过用户名加载用户。该方法的返回值就是UserDetails(本质上是个实体类,Security会自动从里面取值进行对比)。
UserDetails就是用户信息,即:用户名、密码、该用户所具有的权限
源码中的UserDetails接口都有哪些方法:
public interface UserDetails extends Serializable {
//获取用户的权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//账号是否没过期
boolean isAccountNonExpired();
//账号是否没被锁定
boolean isAccountNonLocked();
//密码是否没过期
boolean isCredentialsNonExpired();
//账户是否可用
boolean isEnabled();
}
我们把这些信息提供给Spring Security,Spring Security就知道怎么做登录验证了,
这也体现了Springboot的整体理念,配置大于编码,根本不需要我们自己写Controller实现登录验证逻辑。
一个适应于UserDetails的java POJO类,所谓的 UserDetails接口实现就是一些get方法。get方法由Spring Security调用,我们通过set方法或构造函数为 Spring Security提供UserDetails数据(从数据库查询)。
package com.springSecurityDemo.basicserver.config.auth;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* 实现用户信息接口,相当于Security又套了一层的用户实体类
*/
@NoArgsConstructor
@AllArgsConstructor
public class MyUserDetails implements UserDetails {
//**********************************编写UserDetails相关属性
public String password; //密码
public String username;//用户名
public boolean accountNonExpired; //当前账户是否过期
public boolean accountNonLocked; //是否没被锁定
public boolean credentialsNonExpired; //是否没过期
public boolean enabled; // 账户是否可用
Collection<? extends GrantedAuthority> authorities; // 用户权限集合
//*****************通过下面的方法SpringSecuirty获取用户的的相关数据
/**************这几个参数一定要传递好,否则会导致无法登陆,原本重写过来是null,我们应该重写成我们定义的属性传递回去**/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
//账号是否没过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//是否没被锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//密码是否没过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账号是否可用
@Override
public boolean isEnabled() {
return true;
}
//******************************自定义set方法对黑盒子进行赋值让springsecurity进行调用
public void setPassword(String password)
{
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
}
实现三个接口:一是通过userId(用户名)查询用户信息;二是根据用户名查询用户角色列表;三是**通过角色列表查询权限列表。**这里使用的是Mybatis
public interface MyUserDetailsServiceMapper {
//根据userID查询用户信息
@Select("SELECT username,password,enabled\n" +
"FROM sys_user u\n" +
"WHERE u.username = #{userId}")
MyUserDetails findByUserName(@Param("userId") String userId);
//根据userID查询用户角色
@Select("SELECT role_code\n" +
"FROM sys_role r\n" +
"LEFT JOIN sys_user_role ur ON r.id = ur.role_id\n" +
"LEFT JOIN sys_user u ON u.id = ur.user_id\n" +
"WHERE u.username = #{userId}")
List<String> findRoleByUserName(@Param("userId") String userId);
//根据用户角色查询用户权限
@Select({
""
})
List<String> findAuthorityByRoleCodes(@Param("roleCodes") List<String> roleCodes);
}
package com.springSecurityDemo.basicserver.config.auth;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* 实现UserDetailsService,来通过用户名获取用户信息(也是Security的起始验证)
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
//注入之前写的dao接口
@Resource
private MyUserDetailsServiceMapper myUserDetailsServiceMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.加载基础用户信息 MyUserDetails是实现了UserDetails的实体类
MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(username);
if(myUserDetails == null){
throw new UsernameNotFoundException("用户名不存在");
}
//2.加载用户角色列表
List<String> roleCodes = myUserDetailsServiceMapper.findRoleByUserName(username);
//3.通过用户角色列表加载用户的资源权限列表
List<String> authority = myUserDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
//3.1角色是一个特殊的权限,也要添加到查出来的权限列表中,Security中必须有ROLE_前缀(规定标识)
roleCodes.stream()
.map(rc->"ROLE_"+rc) //每个对象前加前缀
.collect(Collectors.toList()); //再转换回List
//4.添加修改好前缀的角色前缀的角色权限
authority.addAll(roleCodes);
//5.把权限类型的权限给UserDetails
myUserDetails.setAuthorities(
//逗号分隔的字符串转换成权限权限类型列表
AuthorityUtils.commaSeparatedStringToAuthorityList(
//List转字符串,逗号分隔
String.join(",",authority)
)
);
return myUserDetails; //全部交给springsecurity
}
}
重写WebSecurityConfigurerAdapter的 configure(AuthenticationManagerBuilder auth)方法
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// //静态配置用户
// auth.inMemoryAuthentication()
// .withUser("user")
// .password(passwordEncoder().encode("123456"))
// .roles("user")
// .and()
// .withUser("admin")
// .password(passwordEncoder().encode("123456"))
// .authorities("sys:log","sys:user") //赋予资源id,放行其访问资源
// //.roles("admin")
// .and()
// .passwordEncoder(passwordEncoder());//配置BCrypt加密
//从数据库中动态加载用户信息与权限
//把做好的一系列myUserDetailsService信息交给security,并且设置加密方式
auth.userDetailsService(myUserDetailsService)
.passwordEncoder(passwordEncoder());
}
使用BCryptPasswordEncoder,表示存储中(数据库)取出的密码必须是经过BCrypt加密算法加密的。
简单说“资源鉴权规则”就是:你有哪些权限?这些权限能够访问哪些资源?即:权限与资源的匹配关系。
//权限校验规则
.authorizeRequests()
//login页面和login的url谁都可以访问
.antMatchers("/login.html","/login").permitAll()
// //权限表达式的使用:访问该url需要admin角色或ROLE_admin权限
// .antMatchers("/system/*").access("hasAnyRole('admin') or hasAnyAuthority('ROLE_admin')")
.antMatchers("/index").authenticated() //首页是只要登录了就可以访问
//使用权限表达式规则 将自定义权限规则传入,所有url必须走我们写的权限规则方法,才能访问
.anyRequest().access("@rbcaService.hasPermission(request,authentication)")
package com.springSecurityDemo.basicserver.config.auth;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Component("rbcaService") //给这个bean取名
public class MyRBACService {
@Resource
private MyRBACServiceMapper rbacServiceMapper;
//security提供的工具类
private AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 判断某用户是否有该请求资源的访问权限
*
* @param request
* @param authentication
* @return
*/
public boolean hasPermission(HttpServletRequest request,
Authentication authentication) {
//从security中拿出用户主体,实际上是我们之前封装的UserDetials,
//但是又被封了一层
Object principal = authentication.getPrincipal();
//如果取出的principal是我们放进去的UserDetails类,并且已经登录
if (principal instanceof UserDetails) {
//1.强转获取name
String username = ((UserDetails) principal).getUsername();
//2.从内存中获取权限(因为已经登录),放入security容器中,如果有的话返回true
List<GrantedAuthority> authorityList =
AuthorityUtils.commaSeparatedStringToAuthorityList(request.getRequestURI());
return userDetails.getAuthorities().contains(authorityList.get(0));
//2.通过用户名获取用户资源(用户找角色,角色找资源)(这里拿url做的标识,所以是url)
// List urlByUserName = rbacServiceMapper.findUrlByUserName(username);
//3.遍历urls,然后通过antPathMatcher判断是否匹配,匹配的上返回true
// return urlByUserName.stream().anyMatch(
// url -> antPathMatcher.match(url, request.getRequestURI())
// );
}
return false;
}
}
鉴权加载规则与方法级别的权限验证与参数验证略,可以自己找资料如果需要