Version: Next

从数据库动态加载用户权限

  • 查询用户名、密码、用户状态
SELECT username, password, enabled
FROM sys_user u
WHERE u.username = 'admin'
  • 查询目标用户的角色
SELECT r.role_code
FROM sys_role r
LEFT JOIN sys_user_role ur ON r.id = ur.role_id
LEFT JOIN sys_user u ON u.id = ur.user_id
WHERE u.username = 'admin'
  • 查询目标角色可访问的所有url地址,即权限
SELECT m.url
FROM sys_menu m
LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id
LEFT JOIN sys_role r ON r.id = rm.role_id
WHERE r.role_code IN ('admin')

实现两个接口

UserDetailsService

loadUserByUsername方法

  • 通过用户名加载用户
  • 返回值类型为UserDetails

UserDetails

用户信息:即用户名、密码、用户权限

public interface UserDetails extends Serializable {
// 用户权限集合
Collection<? extends GrantedAuthority> getAuthorities();
// 密码
String getPassword();
// 用户名
String getUsername();
// 账号是否过期
boolean isAccountNonExpired();
// 账号是否被锁定
boolean isAccountNonLocked();
// 密码是否过期
boolean isCredentialsNonExpired();
// 账户是否可用
boolean isEnabled();
}

实现这个接口,从数据库读取数据

  • 自己定义一些成员变量
  • 自己定义setter方法,用来从数据读取并设置这些值
public class MyUserDetails implements UserDetails {
String password; //密码
String username; //用户名
boolean accountNonExpired; //是否没过期
boolean accountNonLocked; //是否没被锁定
boolean credentialsNonExpired; //是否没过期
boolean enabled; //账号是否可用
Collection<? extends GrantedAuthority> authorities; //用户的权限集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return 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 enabled;
}
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;
}
}

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({
"<script>",
"SELECT url " ,
"FROM sys_menu m " ,
"LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id " ,
"LEFT JOIN sys_role r ON r.id = rm.role_id ",
"WHERE r.role_code IN ",
"<foreach collection='roleCodes' item='roleCode' open='(' separator=',' close=')'>",
"#{roleCode}",
"</foreach>",
"</script>"
})
List<String> findAuthorityByRoleCodes(@Param("roleCodes") List<String> roleCodes);
}

启动类添加MapperScan注解,开启Mapper扫描

@MapperScan("com.bsx.securityoauth2")
public class Securityoauth2Application { ...}

使用测试类,得到密码的加密密文,存到数据库中

@SpringBootTest
class Securityoauth2ApplicationTests {
@Resource
PasswordEncoder passwordEncoder;
@Test
void contextLoads() {
System.out.println(passwordEncoder.encode("123456"));
}
}
$2a$10$7lUUrndidqCWodinan7V3.OvXjO795PpuAlb86vj8G5OSvUNWahSm

Service

实现UserDetailservice接口

  • 对于loadUserByUsername方法频繁访问数据库获取用户信息的情况,可以采用Spring Cache缓存来处理

接口定义

public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
@Service
public class MyUserDetialService implements UserDetailsService {
@Resource
private MyUserDetailsServiceMapper userDetailsServiceMapper;
/***
* 1. 加载用户基本信息
* 2. 加载用户角色列表
* 3. 通过用户角色列表加载用户的资源权限列表
* @param username 用户名
* @return 从数据库查出来的用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//获得用户信息
MyUserDetails myUserDetails = userDetailsServiceMapper.findByUserName(username);
if (myUserDetails == null) {
throw new UsernameNotFoundException("用户不存在");
}
//获得用户角色列表
List<String> roleCodes = userDetailsServiceMapper.findRoleByUserName(username);
//通过角色列表获取权限列表
List<String> authority = userDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
// 角色是一种特殊的权限
//为角色标识加上ROLE_前缀(Spring Security规范)
roleCodes = roleCodes.stream()
.map(roleCode -> "ROLE_" + roleCode)
.collect(Collectors.toList());
//角色是一种特殊的权限,所以合并
authority.addAll(roleCodes);
//转成用逗号分隔的字符串,为用户设置权限标识
myUserDetails.setAuthorities(
AuthorityUtils.commaSeparatedStringToAuthorityList(
String.join(",", authority)
)
);
return myUserDetails;
}
}

在Spring Security进行配置

SecurityConfig

  • 注入MyUserDetailService
@Configuration
@SuppressWarnings("all")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
private MyAuthenticationFaliureHandler myAuthenticationFaliureHandler;
@Resource
private MyUserDetialService myUserDetialService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
.formLogin()
.loginPage("/login.html")//用户未登录时,访问任何资源都转跳到该路径,即登录页面
.loginProcessingUrl("/login")//登录表单form中action的地址,也就是处理认证请求的路径
.usernameParameter("username")///登录表单form中用户名输入框input的name名,不修改的话默认是username
.passwordParameter("password")//form中密码输入框input的name名,不修改的话默认是password
// .defaultSuccessUrl("/index")//登录认证成功后默认转跳的路径
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFaliureHandler)
.and()
.authorizeRequests()
.antMatchers("/login.html", "/login").permitAll()//不需要通过登录验证就可以被访问的资源路径
.antMatchers("/biz1", "/biz2") //需要对外暴露的资源路径
.hasAnyAuthority("ROLE_user", "ROLE_admin") //user角色和admin角色都可以访问
// .antMatchers("/syslog", "/sysuser")
// .hasAnyRole("admin") //admin角色可以访问
.antMatchers("/syslog").hasAuthority("/syslog")
.antMatchers("/sysuser").hasAuthority("/sysuser")
.anyRequest().authenticated();
}
@Override
protected 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")
// .roles("admin")
// .and()
// .passwordEncoder(passwordEncoder());//配置BCrypt加密
auth.userDetailsService(myUserDetialService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(WebSecurity web) throws Exception {
//将项目中静态资源路径开放出来
web.ignoring().antMatchers("/css/**", "/fonts/**", "/img/**", "/js/**");
}
}