Security
1、基本原理,三个基本的过滤器
security本质只是一个过滤器链
我通过三个最典型过滤器的源码来查看
- FilterSecurityInterceptor: 是一个方法集的权限过滤器,基本位于过滤器的最低端
- ExceptionTranslationFilter: 是一个异常过滤器,用来处理在认证授权过程中抛出的异常
- UsernamePasswordAuthenticationFilter: 对/login的POST请求做拦截,检验表单中用户名,密码
1.1、FilterSecurityInterceptor
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
invoke(new FilterInvocation(request, response, chain));
}
我们来看一下他的invoke方法是怎么执行的
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// first time this request being called, so perform security checking
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
- InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
- 这句话表示他这个过滤器之前还有过滤器,如果之前过滤器做了放行操作,那才往下继续执行,通过filterInvocation.getChain().doFilter执行
1.2、ExceptionTranslationFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, securityException);
}
}
- 异常处理的三个方法
- 1、handleSpringSecurityException
- 2、handleAuthenticationException
- 3、handleAccessDeniedException
1.3、UsernamePasswordAuthenticationFilter
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request); //得到用户名
username = (username != null) ? username : "";
username = username.trim(); //对用户名进行修剪,去掉
String password = obtainPassword(request); //得到密码
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
-
先判断是否是POST提交,然后得到用户的账号密码,用String类里的trim方法去除账号密码前后的空格,然后对其进行验证
-
如果认证成功,他会去执行他的父类AbstractAuthenticationProcessingFilter中的successfulAuthentication方法
-
如果认证失败,他会去执行中的unsuccessfulAuthentication方法
2、思考:过滤器链是如何进行加载的?
源码分析
1、使用SpringSecurity配置过滤器
- DelegatingFilterProxy 翻译:授权 过滤器 代理
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
- delegateToUse = initDelegate(wac);
- 通过initDelegate来进行初始化,我们点进它的源码
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName();//***********FilterChainProxy
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = wac.getBean(targetBeanName, Filter.class);//我们通过wac.getBean得到一个名字,
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
-
Filter delegate = wac.getBean(targetBeanName, Filter. Class);
-
我们通过wac.getBean得到一个FilterChainProxy,我们找到他的源码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
doFilterInternal(request, response, chain);
return;
}
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);//************
}
catch (RequestRejectedException ex) {
this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
- doFilterInternal(request, response, chain);
- 进入他的方法
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.size() == 0) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);//*************
return;
}
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}
- List filters = getFilters(firewallRequest);
- 这条语句只要是得到我们Security中所有的过滤器给他放到过滤链中,我们进入getFilters方法
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
for (SecurityFilterChain chain : this.filterChains) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
this.filterChains.size()));
}
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
- 对过滤器链进行一个迭代 ,然后用getFilter()方法对他进行一个加载,它里面有一个SecurityFilterChain,点进去发现有一个getFilter()的方法,点左边找到他的实现类
@Override
public List<Filter> getFilters() {
return this.filters;
}
3、两个重要接口
3.1、UserDetailsService 接口:
查询数据库用户名和密码
-
创建类继承 UsernamePasswordAuthenticationFilter 重写里面的三个方法
- attemptAuthentication 方法得到用户名密码进行验证
- successfulAuthentication
- unsuccessfulAuthentication
-
创建类实现 UserDetailsService,编写查询数据过程,返回user对象,这个user对象是安全框架提供对象
3.2、PassWordEncoder 接口:
数据加密接口,用于返回User对象里面密码加密
4.web权限认证
4.1、认证
1、设置登陆的用户名和密码三种方式
-
通过配置文件 application.properties
-
spring.security.user.name=wangzhenwei spring.security.user.password=123456
-
-
通过配置类
-
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder passwordEncoder=new BCryptPasswordEncoder(); String password = passwordEncoder.encode("123");//加密 auth.inMemoryAuthentication().passwordEncoder(passwordEncoder).withUser("wzw").password(password).roles(""); //新版本要加上.passwordEncoder(passwordEncoder)不然报错 } }
-
如果不用.passwordEncoder(passwordEncoder),就可以选下面这种方式
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder passwordEncoder=new BCryptPasswordEncoder(); String password = passwordEncoder.encode("123");//加密 auth.inMemoryAuthentication().withUser("wzw").password(password).roles(""); } @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
-
-
自定义编写实现类
第一步:创建配置类,设置使用哪个UserDetailsService实现类
@Configuration public class SecurityConfigDiy extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
第二部:编写实现类,返回User对象,User对象有用户名和操作权限
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//这里的s就是你的用户名
List<GrantedAuthority> auths= AuthorityUtils
.commaSeparatedStringToAuthorityList(String.join("role"));
return new User("wzw",new BCryptPasswordEncoder().encode("123"),auths);
}
}
例子(这是我自己的项目写的)
- SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
@Override//过滤静态资源
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/sys/**","/favicon.ico","/error");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().accessDeniedPage("/login.html");
http.sessionManagement()
.maximumSessions(1)//允许同时登陆的人数
.maxSessionsPreventsLogin(false)//false表示允许再次登录,之前的账号会被踢下线
.expiredSessionStrategy(new CustomExpiredSessionStrategy());//
http.cors();//允许跨域
//退出登录 logoutSuccessUrl退出成功后去哪
http.logout().
logoutUrl("/logout")
.logoutSuccessUrl("/login.html")
.deleteCookies("JSESSIONID")
.permitAll();
//配置没有权限访问的页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
System.out.println(password());
http.formLogin()
.loginPage("/login.html")//登录页面地址
.loginProcessingUrl("/user/login")//登录访问页面路径 action下的地址
.defaultSuccessUrl("/index", true)//登陆成功后,跳转路径
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll()//设置哪些路径不需要认证
.anyRequest().authenticated()//所有请求都要认证
.and()
.csrf().disable()//关闭csrf防护
.headers().frameOptions().disable();//允许iframe页面嵌套
}
}
- MyUserDetailService
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String encode = "$2a$10$5pHsIbuYPY1Kw7jyJYR6quJTgQ2U/RJX.TXqbnYm0fNHHi3FhPmN.";
System.out.println(encode);
List<GrantedAuthority> auths= AuthorityUtils.commaSeparatedStringToAuthorityList(String.join("role"));
return new User("wzw",encode,auths);
}
}
4.2、授权
基于角色或权限进行访问控制
四个方法:
4.2.1、hasAuthority 方法
-
如果当前的主体具有指定的某一个权限,则返回ture,否则返回false
-
1、在配置类 config 设置当前访问地址有哪些权限
-
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() /*当前登录用户只有具有相应的权限才能访问接口*/ .antMatchers("接口").hasAuthority("权限"); }
-
-
2、在UserDetailsService,把返回User对象设置权限,授权
-
List<GrantedAuthority> auths= AuthorityUtils .commaSeparatedStringToAuthorityList(String.join("role")); return new User("wzw",encode,auths);
-
-
4.2.2、hasAnyAuthority
同理 config
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
/*当前登录用户只有具有相应的权限才能访问接口*/
.antMatchers("接口").hasAnyAuthority("权限1,权限2")
}
4.2.3、hasRole
4.2.4、HasAnyRole
我们先看一下源码
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(!role.startsWith("ROLE_"),
() -> "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
return "hasRole('ROLE_" + role + "')";
}
可以看出hasRole是要有"ROLE_"前缀
http.authorizeRequests()
/*当前登录用户只有具有相应的角色才能访问接口*/
.antMatchers("接口").hasAnyRole("角色")
/*当前登录用户只有具有相应的角色才能访问接口*/
.antMatchers("接口").hasRole("角色")
}
- UserDetailsService
List<GrantedAuthority> auths= AuthorityUtils.commaSeparatedStringToAuthorityList(String.join("ROLE_角色"));
return new User("wzw",encode,auths);
4.2.5、无权限页面跳转
http.exceptionHandling().accessDeniedPage("/403页面");
5、注解的使用
使用注解前要先开启注解功能
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)//开启
public class ValidatorApplication {
public static void main(String[] args) {
SpringApplication.run(ValidatorApplication.class, args);
}
}
5.1、@Secured
-
用户具有某个角色,可以访问方法
@GetMapping("/test") @Secured({"ROLE_角色1","ROLE_角色2"}) public String test(){ return "successful"; }
5.2、@PreAuthorize
-
@PreAuthorize注解适合进入方法前的权限验证, 可以将登录用户的roles/permissions参数传入方法中
-
@GetMapping("/test") // @PreAuthorize("hasRole('角色')") // @PreAuthorize("hasAnyRole('角色1','角色2')") // @PreAuthorize("hasAuthority('权限')") // @PreAuthorize("hasAnyAuthority('权限1,权限2')") public String test(){ return "successful"; }
5.3、@PostAuthorize
- @PostAuthorize注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限
- 方法的使用和@PreAuthorize一样
5.4、@PostFilter
- @PostFilter 注解是对方法返回数据进行一个过滤
5.5、@PreFilter
- @PreFilter 注解是对数据进入控制器之前对数据进行一个过滤
同理
6、用户注销
http.logout().logoutUrl("接口").logoutSuccessUrl("退出成功的页面").permitAll();
PreAuthorize(“hasAnyRole(‘角色1’,‘角色2’)”)
// @PreAuthorize(“hasAuthority(‘权限’)”)
// @PreAuthorize(“hasAnyAuthority(‘权限1,权限2’)”)
public String test(){
return “successful”;
}
### 5.3、@PostAuthorize
- @PostAuthorize注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限
- 方法的使用和@PreAuthorize一样
### 5.4、@PostFilter
- @PostFilter 注解是对方法返回数据进行一个过滤
- [外链图片转存中...(img-S8gxFmFp-1633768328894)]
### 5.5、@PreFilter
- @PreFilter 注解是对数据进入控制器之前对数据进行一个过滤
同理
## 6、用户注销
```java
http.logout().logoutUrl("接口").logoutSuccessUrl("退出成功的页面").permitAll();