SpringSecurity Web 权限方案

配置类伪代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login_page")
.passwordParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/sign_in")
.permitAll()
.and().authorizeRequests().antMatchers("/test").hasRole("test")
.anyRequest().authenticated().accessDecisionManager(accessDecisionManager())
.and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().disable();
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
http.addFilterAfter(new MyFittler(), LogoutFilter.class);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
配置简介
configure(AuthenticationManagerBuilder auth)
AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);如果想要使用该功能你需要配置一个 UserDetailService
和 PasswordEncoder
。
- UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户
- PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。
configure(WebSecurity web)
这个配置方法用于配置静态资源的处理方式,可使用 Ant 匹配规则。
configure(HttpSecurity http)
http
.formLogin()
.loginPage("/login_page")
.passwordParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/sign_in")
.permitAll()
2
3
4
5
6
7
这是配置登录相关的操作。从方法名可知,配置了登录页请求路径,密码属性名,用户名属性名,和登录请求路径,permitAll()
代表任意用户可访问。
http
.authorizeRequests()
.antMatchers("/test").hasRole("test")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager());
2
3
4
5
以上配置是权限相关的配置,配置了一个 /test url 该有什么权限才能访问, anyRequest()
表示所有请求,authenticated() 表示已登录用户才能访问, accessDecisionManager()
表示绑定在 url 上的鉴权管理器
为了对比,现在贴出另一个权限配置清单:
http.authorizeRequests()
.antMatchers("/tets_a/**","/test_b/**").hasRole("test")
.antMatchers("/a/**","/b/**").authenticated()
.accessDecisionManager(accessDecisionManager())
2
3
4
我们可以看到权限配置的自由度很高,鉴权管理器可以绑定到任意 url 上;而且可以硬编码各种 url 权限:
http
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
2
3
4
登出相关配置,这里配置了登出 url 和登出成功处理器:
http
.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
2
3
上面代码是配置鉴权失败的处理器。
http.addFilterAfter(new MyFittler(), LogoutFilter.class);
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
2
上面代码展示如何在过滤器链中插入自己的过滤器,addFilterBefore 加在对应的过滤器之前,addFilterAfter 加在对应的过滤器之后,addFilterAt 加在过滤器同一位置,事实上框架原有的 Filter 在启动 HttpSecurity 配置的过程中,都由框架完成了其一定程度上固定的配置,是不允许更改替换的。根据测试结果来看,调用 addFilterAt 方法插入的 Filter ,会在这个位置上的原有 Filter 之前执行。
关于 HttpSecurity 使用的是链式编程,其中 http.xxxx.and.yyyyy 这种写法和 http.xxxx;http.yyyy 写法意义一样。
自定义 AuthenticationManager 和 AccessDecisionManager
重写 authenticationManagerBean() 方法,并构造一个 authenticationManager:
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
ProviderManager authenticationManager = new ProviderManager(Arrays.asLis(getMyAuthenticationProvider(),daoAuthenticationProvider()));
return authenticationManager;
}
2
3
4
5
我这里给 authenticationManager 配置了两个认证器,执行过程参考流程图。
定义构造AccessDecisionManager的方法并在配置类中调用,配置参考 configure(HttpSecurity http) 说明:
public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new MyExpressionVoter(),
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);
}
2
3
4
5
6
7
8
9
投票管理器会收集投票器投票结果做统计,最终结果大于等于0代表通过;每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。
Security 权限系统
UserDetails
Security 中的用户接口,我们自定义用户类要实现该接口。
GrantedAuthority
Security 中的用户权限接口,自定义权限需要实现该接口:
public class MyGrantedAuthority implements GrantedAuthority {
private String authority;
}
2
3
authority 表示权限字段,需要注意的是在 config 中配置的权限会被加上 ROLE_ 前缀,比如我们的配置 authorizeRequests().antMatchers("/test").hasRole("test"),配置了一个 test 权限但我们存储的权限字段(authority)应该是 ROLE_test 。
UserDetailsService
Security 中的用户 Service,自定义用户服务类需要实现该接口:
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return.....
}
}
2
3
4
5
6
7
loadUserByUsername的作用在上文中已经说明,就是根据用户名查询用户对象。
SecurityContextHolder
用户在完成登录后 Security 会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得,用户信息被封装成 SecurityContext ,而实际存储的类是 SecurityContextHolderStrategy ,默认的SecurityContextHolderStrategy 实现类是 ThreadLocalSecurityContextHolderStrategy 它使用了ThreadLocal来存储了用户信息。
手动填充 SecurityContextHolder 示例:
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
SecurityContextHolder.getContext().setAuthentication(token);
2
对于使用 token 鉴权的系统,我们就可以验证token后手动填充SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空SecurityContextHolder。
设置登录系统的账号、密码
方式一:application 配置文件
spring:
security:
user:
name: hong
password: 123
2
3
4
5
方式二:配置类
/**
* 1.创建一个配置类继承自WebSecurityConfigurerAdapter
* 2.重写configure(AuthenticationManagerBuilder auth)方法, 设置用户名和密码
* 3.添加PasswordEncoder组件, 可能是用来解码的吧
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// BCryptPasswordEncoder类可以给字符串进行加密
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("Tom").password(password).roles("admin");
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
方式三:实现 UserDetailsService 接口
/**
* 1.创建一个配置类继承自WebSecurityConfigurerAdapter
* 2.重写configure(AuthenticationManagerBuilder auth)方法, 给auth传入一个UserDetailsService接口的实现类
* 3.编写一个UserDetailsService的实现类, 重写loadUserByUsername方法, 进行用户名判断, 并返回UserDetails对象交给SpringSecurity进行密码的判断
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 密码解析器:
* 作用:
* 1.SpringSecurity会使用此组件对密码进行加密,
* 2.SpringSecurity会使用此组件对userDetailsService返回UserDetails对象中保存的密码与前端发送的密码进行比较
*
* PasswordEncoder接口定义的方法的含义:
* encode: 对密码进行加密
* matches: 前端提交的原始密码与UserDetails对象中保存的密码进行比较, 如果一致返回true, 反之false
* upgradeEncoding: 是否需要再次加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
实现 UserDetailsService 接口
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
/**
* 作用: 进行用户名的判断
* 运行流程: 这个方法会传入一个用户名, 我们可以根据用户名进行查询, 并将查询结果交给SpringSecurity,
* SpringSecurity会使用PasswordEncoder密码解析器帮我们进行密码的判断操作, 我们需要负责用户名的判断。
* 总结: SpringSecurity会根据我们返回的UserDetails对象进行密码的判断, 但是并不会判断用户名,
* 因此我们需要编写用户名的判断操作
* @param username 登录时传入的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(!"Jerry".equals(username)){
throw new UsernameNotFoundException("用户不存在");
}
String password = new BCryptPasswordEncoder().encode("123");
// 在这里可以给登录用户设置权限和角色, 使用逗号分割, xxx为权限, ROLE_xxx为角色
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User("Jerry", password, auths);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23