学习基于记录,而不止于记录。
希望自己能坚持下去~
0.写在前面
继续上一篇,本篇记录在spring security加入持久层,进行动态权限控制,由静态配置改为动态读取。
spring boot版本:2.2.7.RELEASE
开发工具:IntelliJ IDEA 2018.3.2 (Ultimate Edition)
jdk: java version "1.8.0_181"
maven: 3.3.9
持久层:jpa(这个不是重点,根据你的需要使用)
1.UserDetail
spring security提供UserDetails接口,对用户信息进行包装:分为两部分,基本信息,包括登录用户名、密码、账户状态等;权限信息,权限集合。
我们需要实现这个接口,并且提供setter方法,因为security会调用getter方法获取信息,我们必须在实例化的时候将属性值初始化,这里为了简化测试,一些基本信息比如账户过期状态设置为一直不过期,代码如下:
public class MyUserDetails implements UserDetails {
String password; //密码
String username; //用户名
boolean accountNonExpired; //账户是否没过期
boolean accountNonLocked; //账户是否没锁定
boolean credentialsNonExpired; //密码是否没过期
boolean enabled; //是否启用
Collection<? extends GrantedAuthority> authorities; //用户权限集合
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;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return 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;
}
}
2.UserDetailService
UserDetailService会根据用户名(这里的用户名并不一定是用户名称而是用户唯一标识)加载用户信息,提供给spring security用于校验用户信息。如此,就不需要硬编码配置了。
@Service
public class MyUserDetailsSerive implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
MenuRepository menuRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//用户基础数据
Optional<User> userOptional = userRepository.findById(Integer.valueOf(username));
User user;
if (userOptional.isPresent()) {
user = userOptional.get();
} else {
return null;
}
MyUserDetails userDetails = new MyUserDetails();
userDetails.setUsername(String.valueOf(user.getId()));
userDetails.setEnabled(user.getEnabled() == 1);
userDetails.setPassword(user.getPassword());
//用户权限集合
List<String> authorities = user.getRoles().parallelStream()
.map(r -> r.getMenus().stream().map(Menu::getUrl).collect(Collectors.toList()))
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
userDetails.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(
String.join(",", authorities)
));
System.out.println("authorities:" + authorities);
return userDetails;
}
}
接下来将上述java注入到SecurityConfig中,然后提供给配置调用,代码如下:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsSerive)
.passwordEncoder(passwordEncoder()); //配置加密方式
}
3.动态鉴权
用户信息已经可以从数据库中加载并校验了,那么用户的每一次访问也必须纳入到security校验之中,编写以下校验方法。
@Service("myRBACService")
public class MyRBACService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
System.out.println("request.getRequestURI():" + request.getRequestURI());
UserDetails userDetails = (UserDetails) principal;
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(request.getRequestURI());
System.out.println("userDetails.getAuthorities():" + userDetails.getAuthorities());
return userDetails.getAuthorities().contains(simpleGrantedAuthority);
}
return false;
}
}
上述方法,有两个参数,第一个参数是request请求,用于获取用户请求的接口资源;第二个是用户登陆成功后生成的authentication,用于鉴权,里面有当前登录用户的所有权限信息。两者结合匹配结果便可以告知是否允许访问。
在SecurityConfig修改配置如下:
http.
.authorizeRequests()
.antMatchers("/login.html", "/login", "/expired", "/jwt-expired.html").permitAll()
.anyRequest().access("@myRBACService.hasPermission(request,authentication)")
在此贴下一下完整的配置:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
MyExpiredSessionStrategy myExpiredSessionStrategy;
@Autowired
MyUserDetailsSerive myUserDetailsSerive;
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭这项拦截功能,要不然后面测试都无法进行
http.csrf().disable();
http.formLogin()
.loginPage("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
.failureHandler(myAuthenticationFailureHandler)
.successHandler(myAuthenticationSuccessHandler)
.and()
.authorizeRequests()
.antMatchers("/login.html", "/login", "/expired", "/jwt-expired.html").permitAll()
.anyRequest().access("@myRBACService.hasPermission(request,authentication)")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionFixation()
.migrateSession()
//最大登陆人数为1
.maximumSessions(1)
//设置false允许多点登录但是,如果超出最大人数之前的登录会被踢掉
.maxSessionsPreventsLogin(false)
.expiredSessionStrategy(myExpiredSessionStrategy);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsSerive)
.passwordEncoder(passwordEncoder()); //配置加密方式
}
//加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.数据库
根据RBAC(Role-Based Access Contro)权限访问控制模型,搭建数据库,并且提供模拟数据。
这里就不贴代码了,直接给源码地址(里面包含sql文件)。
5.总结
虽然公司里面使用spring security作为安全管理,但是自己在使用过程中有不少存疑的地方,这次梳理到现在,好歹是有点头绪了,后续还会更新前后端分离开发环境下的安全配置,也就是无状态的安全控制。这篇博客本来应该去年6.8发布,一直拖到现在,实在是惭愧。