package com.lagou.securitydemo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
*/
@RestController
public class HelloSecurityController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
CREATE DATABASE security_management CHARACTER SET utf8
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-
instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-
4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.lagougroupId>
<artifactId>lagou-security-managementartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>lagou-security-managementname>
<description>lagou-security-managementdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.21version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
package com.lagou.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.
builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
//这里只需要对应的三个方法即可,并不需要都进行重写,配置类的注解是核心
//使得可以从ioc里得到,并进行注入操作(一般只能是得到WebSecurityConfigurerAdapter类型或者其子类)
//所以需要继承WebSecurityConfigurerAdapter类,从而操作如下的方法,当然这是底层的缘故
//若没有注入,自然对应的变量一般会设置为null了,实际上可能以前也有过说明,没有注入,就是null
//在正常情况下,没有注入是会报错的,但是却可以进行判断,然后赋值,具体方式,可以百度进行查找
//大体思路是,我们得到容器(当然这是很简单,一般底层都可以得到,要不然怎么操作注入呢)
//然后循环,判断是否是WebSecurityConfigurerAdapter类的父类,然后就决定是否赋值为null了
//所以我们在特殊情况下,我们也认为没有注入则是null
//当然,大多数都是报错的
//可能以前或者以后的博客并没有说明,但一般不会,注意即可,因为总不能不会忘记吧,或者出错吧
//当为null时,那么对应的方法也就操作不了,也就不会进行设置,或者使得初始化的信息等等
//如使得不操作默认认证(或者说认证页面)了
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth); //一般来说,重写父类的方法,一般会执行父类的方法
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//选择(开启)HttpBasic认证模式
//注意:分开的话(其他行的分开,不能是一行,否则在注释"//"后面的也会认为是注释内容)
//是可以使用注释的,不会影响运行,大多数的语言都可以
http.httpBasic() //开启HttpBasic认证
.and().authorizeRequests().anyRequest().authenticated();
//and()进行拼接作用
//authorizeRequests()授权我们的请求
//anyRequest()所有的请求(自然也包括静态资源的请求等等)
//authenticated()都需要认证
//即所有请求,都需要认证之后才可访问(即认证后进行授权,就可访问了,这时基本所有的请求都授权了)
}
}
//最后注意:这些方法的运行,在启动类还没启动完毕时进行初始化的,即,那时候进行执行,可以使用断点进行验证
http.formLogin() //开启表单认证
.and().authorizeRequests().anyRequest().authenticated();
//我们发现,除了认证的代码,后面都是一样的,当然了,对应的操作本来就是符合认证的,一样的也是正确的
http.formLogin().loginPage("/login.html") //开启表单认证
.and().authorizeRequests().anyRequest().authenticated();
//我们发现,除了认证的代码,后面都是一样的,当然了,对应的操作本来就是符合认证的,一样的也是正确的
//其中loginPage("/login.html")将默认使用代码生成的页面(一般是这样),变成我们自己的页面
http.formLogin().loginPage("/login.html") //开启表单认证
.and().authorizeRequests().
antMatchers("/login.html").permitAll() //放行对应的页面,而不经过验证过滤
.anyRequest().authenticated();
http.formLogin().loginPage("/toLoginPage") //开启表单认证,记得在对应的LoginController类里面进行操作
.and().authorizeRequests().
antMatchers("/toLoginPage").permitAll()
//放行对应的页面,而不经过验证过滤,基本操作所有放行
.anyRequest().authenticated();
//其中记得都变成toLoginPage,因为放行需要对应,不对应的怎么会放行呢,是吧
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题,基本只能操作认证的放行
web.ignoring().antMatchers("/css/**","/images/**","/js/**");
}
http.formLogin().loginPage("/toLoginPage") //开启表单认证
.and().authorizeRequests().
antMatchers("/toLoginPage","/css/**","/images/**","/js/**").permitAll()
//放行对应的页面,而不经过验证过滤
.anyRequest().authenticated();
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/toLoginPage","/css/**","/images/**","/js/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage") //开启表单认证
.and().authorizeRequests()
//当然,对应的"."也可以放在这里(后面),只是不整齐而已,或者所有的点写后面,只是不美观
//这里就都写在前面了,后续也是如此
//放行对应的页面,而不经过验证过滤
.anyRequest().authenticated();
}
//也就是说,将放行都给configure(WebSecurity web)方法
//实际上该方法也可以用来操作资源的放行的(容易一点),且也可以操作某些安全控制(只是难一点而已)
//下面的configure(HttpSecurity http)方法也可以操作放行(容易一点)
//因为存在permitAll()方法,且必须要这个(难一点),否则后面的方法延续不能进行,会报错
//所以总体来说,WebSecurity web大于HttpSecurity http
//根据难易程度来说,操作安全控制,一般由HttpSecurity http来操作,放行,由WebSecurity web来操作
@Override
protected void configure(HttpSecurity http) throws Exception {
//可以理解为UsernamePasswordAuthenticationFilter就是操作formLogin(),后面的都是进行设置
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1") //设置表单提交的路径,而不是默认的/login了
//这样也可以访问/login1到对应的页面里面
.usernameParameter("username1") //设置对应用户名的name
.passwordParameter("password1") //设置对应密码的name
//上面的设置,使得前端的对应字段需要按照他这样的格式即可
//当然,若没有按照,那么自然后端返回的是null了
//也就不会认证成功了
.successForwardUrl("/")
//登录成功后,跳转的路径,一般情况下,我们访问时
//后端会得到对应的访问路径,使得跳转到对应的路径
//当然,我们可以指定路径的跳转,上面就是指定路径跳转到"/"下,这个项目就是index.html了
.and().authorizeRequests().anyRequest().authenticated();
}
<input name="_csrf" type="hidden" value="4ac3b39d-7cc3-42bd-ac6c-51b5da93e454" />
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
//设置表单提交的路径,而不是默认的/login了,但是却不能直接的访问了
//默认操作post,所以操作get会访问失败,自然的又会回到该页面
.usernameParameter("username1") //设置对应用户名的name
.passwordParameter("password1") //设置对应密码的name
//上面的设置,使得前端的对应字段需要按照他这样的格式即可
//当然,若没有按照,那么自然后端返回的是null了
//也就不会认证成功了
.successForwardUrl("/")
//登录成功后,跳转的路径(是转发的操作,而不是重定向),一般情况下,我们访问时
//后端会得到对应的访问路径,使得跳转到对应的路径
//当然,我们可以指定路径的跳转,上面就是指定路径跳转到"/"下,这个项目就是index.html了
.and().authorizeRequests().anyRequest().authenticated();
//这下面就是加上的代码
//关闭csrf防护,会使得不会生成对应的_csrf值,所以前端得到值会出现报错,当然这是后面的操作
//即他并不是只有全部不防护的意思,而有直接的不操作_csrf值,使得基本不进行防护
http.csrf().disable();
//在没有其他的操作时,他一般会使得对应的过滤器删除,若有其他操作
//比如
//
//http.csrf().ignoringAntMatchers("/user/saveOrUpdate"); 后面会操作到
//那么不会删除对应的过滤器,即这时无论你是否关闭,即是否添加http.csrf().disable();,都不会删除
//当然,若什么都不写,对应的过滤器自然是存在的,即不会删除
//因为是默认的配置,或者说默认的添加上的(本来是0,最后是15个默认,那不就是添加吗)
//所以说,基本只有单独的http.csrf().disable();才会进行删除
}
<li><a href="/user/findAll" target="right"><span class="icon-caret-right">span>用户管理a>li>
<li><a href="/product/findAll" target="right"><span class="icon-caret-right">span>商品管理a>li>
<iframe scrolling="auto" rameborder="0" src="images/bg.jpg" name="right" width="100%" height="100%">iframe>
//关闭csrf防护
http.csrf().disable();
//加上下面的代码即可
//加载同源域名下的iframe页面
http.headers().frameOptions().sameOrigin();
package com.lagou.service.impl;
import com.lagou.domain.User;
import com.lagou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.Service;
import java.util.Collection;
/**
*基于数据库完成认证
*/
@Service
//只有实现了他,才可以使得注入后当成参数传递,因为参数一般需要对应的UserDetailsService类型或者其子类
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 根据用户名查询用户
* @param username 前端传入的用户名
* @return
* @throws UsernameNotFoundException
*/
//只有该一个接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库得到信息,具体如何得到的,并不需要知道
//因为我们只需要学习对应的spring security,而不是学习该项目
User byUsername = userService.findByUsername(username);
if(byUsername == null){
throw new UsernameNotFoundException("用户没有找到,"+username);
}
//权限的集合,现在我们并不需要操作权限,所以设置为空的集合,但不要设置成null,否则报错
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
username //用户名
,"{noop}"+byUsername.getPassword() //密码
,true //用户是否启用,true代表启用,false代表未启用
,true //用户是否过期,true代表没有过期,false代表过期
,true //用户凭证是否过期,true代表没有过期,false代表过期
,true //用户是否锁定,true代表未锁定,false代表锁定
,authorities //权限的集合
);
//"{noop}"+byUsername.getPassword()代表不使用密码加密
//实际上是{noop}代表了不使用的意思,而该参数正好是密码的参数,所以是不使用密码加密
return userDetails;
}
}
//注意:在认证时,该方法才会进行运行,使得操作密码的方式,当然,用户名需要自己进行验证
//比如说,我们将username修改成"username",会发现,还是可以认证成功
//所以用户名需要我们自己进行验证,对应的认证基本只会认证密码
//至此,如果我们重新运行启动类,没有清除缓存,即认为认证的,那么对应的UserDetails会根据缓存得到
//而不会执行该方法,所以这时该值不变,也就使得后面的得到用户名操作的值不会变
@Autowired
private UserDetailsService userDetailsService;
/**
* 身份安全管理器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService) //交给其管理,使得认证时,操作对应的方法
//我们输入用户名和密码时,当成参数提交,当然
//实际上对应的判断,只会判断密码,所以我们需要自己判断用户名)数据库判断)
//可以执行对应的方法,得到对应的数据库信息的返回结果进行处理(处理密码)
//总体使得只要用户名和密码可以被数据库里进行找到,那么对应的结果自然可以使得认证成功
//那么这个时候,就不会操作默认的用户名和密码了
//那么原来的默认用户名和密码是怎么来的呢:
//是对应的super.configure(auth)操作得到的(得到的也是默认)
//当我们有UserDetailsService的实例(前面的实现)
//那么就不会使得操作默认的用户名和密码,无是否有super.configure(auth)和是否有配置类
//而是操作对应的userDetailsService结果,即看看是否有对应的返回数据,从而完成认证
//当然,若也没有对应的auth.userDetailsService(userDetailsService)操作,对应的实例有无
//也是没有默认的操作
//那么基本不可能能够认证成功的,若没有配置类,且没有对应的实例,则操作默认的,否则什么都没有
//具体自己进行测试
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
/*
bcrypt加密后的字符串形如:
$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
*/
$2a $10 $wouq9P/HNgvYj2jKtUN8rO JJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
username //用户名
,"{bcrypt}"+byUsername.getPassword() //密码
,true //用户是否启用,true代表启用,false代表未启用
,true //用户是否过期,true代表没有过期,false代表过期
,true //用户凭证是否过期,true代表没有过期,false代表过期
,true //用户是否锁定,true代表未锁定,false代表锁定
,authorities //权限的集合
);
//"{bcrypt}"+byUsername.getPassword()代表使用密码加密
//当然,他并不是对他自己进行加密,而是使得他操作我们输入的值进行加密
//即该前缀是携带操作我们输入密码的加密方式的意思
public static void main(String[] args) {
//使用加密的类,进行加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//得到123456加密后的值
String code = bCryptPasswordEncoder.encode("123456");
System.out.println(code); //$2a$10$qbhLCxg43v7bv0oPk1dNz.vXCL.qT4.kEmMRdTJlh/CFY9v.OwAtq
//注意:对应的值,多次的运行并不是一样的,因为有随机的存在
//每个随机数Spring Security会进行保存的,使得可以解密
//当然并不是一定会进行保存,因为解密的操作可能也是需要对应加密后的字符串的,使得得到值
//当然这里并不需要我们进行理解,只需要知道他能这样做即可
}
/**
* 获取当前登录的用户
*/
@GetMapping("/loginUser1")
@ResponseBody
public UserDetails getCurrentUser1(){
UserDetails principal =
(UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println(principal);
return principal;
}
/**
* 获取当前登录的用户
*/
@GetMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser2(Authentication authentication){
UserDetails principal = (UserDetails)authentication.getPrincipal();
System.out.println(principal);
return principal;
}
/**
* 获取当前登录的用户
*/
@GetMapping("/loginUser3")
@ResponseBody
//@AuthenticationPrincipal注解必须加上,否则报错
public UserDetails getCurrentUser3(@AuthenticationPrincipal UserDetails userDetails){
System.out.println(userDetails.getUsername());
System.out.println(userDetails.getPassword());
System.out.println(userDetails);
return userDetails;
}
//上面是三种方式获取用户名
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.and().rememberMe() //这里加上该代码,代表开启记住我功能
.tokenValiditySeconds(1209600) //token失效时间,默认设置为2周(1209600秒)
.rememberMeParameter("remember-mer")
//代表表单里对应的input的name值,一般默认设置为remember-me,这里就设置成remember-mer了
//基本是随便设置的
.and().authorizeRequests().anyRequest().authenticated();
<div class="form-group">
<div>
<input type="checkbox" name="remember-mer" value="true"/>记住我
div>
div>
@Autowired
private DataSource dataSource;
/**
* 负责token与数据库之间的相关操作
* @return
*/
@Bean
public PersistentTokenRepository getpersistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); //设置数据源
//启动时帮助我们创建一张表,主要用来存放token的信息
//第一次启动设置为true,第二次启动设置为false,或者注释掉
//true代表创建表,false代表不创建,当然,注释的话,自然不会操作表
//因为若有对应的表的话,设置为true时,启动时会报错,说明表已经存在
//或者说,只要数据库里有表存在,就需要设置为false或者注释
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository()) //这里是添加的代码
.and().authorizeRequests().anyRequest().authenticated();
/**
* 根据用户ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
public User getById(@PathVariable Integer id) {
//获取认证的信息,相当于可以得到org.springframework.security.core.userdetails.User的信息一样
//只是需要再次获取
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication);
System.out.println(RememberMeAuthenticationToken.class.isAssignableFrom(
authentication.getClass()) == true);
//如果返回true,代表这个登录认证的信息,来源于自动登录
if(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass()) == true){
//一般来说,spring security在操作异常时,会重定向到认证页面,一般并不会打印异常的信息
//但是,这是交给spring security进行操作的异常
//如果我们自己进行捕获,如使用try,则不会重定向到认证页面
throw new RememberMeAuthenticationException("认证来源于RememberMe");
//使用throw自然是直接的操作异常,而没有什么抛出以及捕获的操作
//到最终的抛出和不是最终的捕获的了
//但这是对他自己,直接的操作异常可以被捕获(只要是异常就可以被捕获,基本没有最终的捕获)
//所以他本身也是可以被捕获(try)的
//而抛出则会指向到他这样的格式,所以并不会在抛出了(最终的抛出)
}
User user = userService.getById(id);
return user;
//注意:使用表单登录时,对应的authentication值类型是如下:
//org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fa794fef
//而自动登录,即使用rememberme时,对应的值,如下:
//org.springframework.security.authentication.RememberMeAuthenticationToken@487ac513
//也就是说,对应的操作是有不同的,主要是Authentication认证的信息不同
//虽然他保存了认证信息,但是使用什么方式认证,决定了他的值的类型(是子类或者其本身,则返回true)
//这样就可以使得进行安全的认证,使得有些必须只能够操作表单认证才可
//否则认证的类型不会相同(因为认证后,就不需要认证了)
}
/*
在配置类中,我们可以知道有如下的操作loginPage("/toLoginPage")和successForwardUrl("/")
接下来我们看看对应的loginPage("/toLoginPage"),点击进去
public FormLoginConfigurer loginPage(String loginPage) {
return (FormLoginConfigurer)super.loginPage(loginPage);
}
再次点击loginPage(loginPage);进去
protected T loginPage(String loginPage) {
this.setLoginPage(loginPage);
this.updateAuthenticationDefaults();
this.customLoginPage = true;
return this.getSelf();
}
点击this.updateAuthenticationDefaults();进去
protected final void updateAuthenticationDefaults() {
if (this.loginProcessingUrl == null) {
this.loginProcessingUrl(this.loginPage);
这里是操作没有认证或者是除了表单的认证失败的其他情况,就需要到这里,代表重定向到认证页面
}
if (this.failureHandler == null) {
this.failureUrl(this.loginPage + "?error"); //当表单失败认证时,操作这个,有后缀
}
LogoutConfigurer logoutConfigurer = (LogoutConfigurer)
((HttpSecurityBuilder)this.getBuilder()).getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
}
当退出认证时,操作这个,一般是退出登录,有后缀,基本是重定向
}
上面的后缀,只是用来看的,实际上并不会有什么作用
你可以试着在后面随便怎么操作,如赋值,或者多加,结果都是认证页面,说明并没有什么作用
我们点击this.failureUrl(this.loginPage + "?error");进去
public final T failureUrl(String authenticationFailureUrl) {
T result = this.failureHandler(new
SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
this.failureUrl = authenticationFailureUrl;
return result;
}
点击new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl)进去
会发现SimpleUrlAuthenticationFailureHandler实现了AuthenticationFailureHandler接口
代表操作认证失败的,查看对应的方法,可以知道是重定向
由于该new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl)的当作参数
所以回到上一级(上一级表示,就是我们进入之前的地方),若是多个上一级,我会说明回到某某地方,并标识写出来
当然,并不会都会标识上一级,注意即可,总有漏网之鱼吧
找到this.failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
点击failureHandler进去
找到如下
public final T failureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return this.getSelf();
}
说明failureHandler是操作认证失败的
至此,可以得出认证失败,的确是重定向
那么认证成功呢,点击successForwardUrl("/")进去
public FormLoginConfigurer successForwardUrl(String forwardUrl) {
this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
return this;
}
点击new ForwardAuthenticationSuccessHandler(forwardUrl)进去
会发现ForwardAuthenticationSuccessHandler实现类AuthenticationSuccessHandler接口
说明是操作认证成功的,看看对应的方法
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
发现是转发操作,至此对应的认证成功也的确是转发,回到上一级
由于new ForwardAuthenticationSuccessHandler(forwardUrl)是当成参数,我们点击如下
this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
点击successHandler进去
public final T successHandler(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
return this.getSelf();
}
可以发现successHandler是操作认证成功的
至此可以得出结论,failureHandler操作认证失败,successHandler操作认证成功
但是他们却都是操作表单的认证或者是对应的提交认证
只要提交就会,当然,如HttpBasic认证也有这样的设置,自然也是一样的操作
只是一般没有,所以我们通常操作表单认证
而没有认证,或者不是表单认证失败的(如对比失败),基本都会重定向到认证页面,一般没有后缀
*/
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*自定义登录成功或失败处理器
*/
@Service
public class MyAuthenticationService implements
AuthenticationSuccessHandler,AuthenticationFailureHandler{
//用来得到可以操作响应的response,可以用它来操作重定向(转发好像操作不了)
//虽然单纯的使用response也可以操作(也可以操作转发),但对于ajax来说
//重定向和转发并不会起作用,因为接收者不是浏览器,而是ajax,所以一般会使得报错
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private ObjectMapper objectMapper; //可以将map转换成json
/**
* 登录成功后处理的逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Authentication authentication) throws
IOException, ServletException {
System.out.println("登录成功后继续处理");
redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/");
//httpServletResponse.sendRedirect("/");
//RequestDispatcher requestDispatcher = httpServletRequest.getRequestDispatcher("/");
//requestDispatcher.forward(httpServletRequest, httpServletResponse);
}
/**
* 登录失败的处理逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AuthenticationException e) throws
IOException, ServletException {
System.out.println("登录失败后进行处理");
redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/toLoginPage");
//httpServletResponse.sendRedirect("/toLoginPage");
//RequestDispatcher requestDispatcher = httpServletRequest.getRequestDispatcher("/toLoginPage");
//requestDispatcher.forward(httpServletRequest, httpServletResponse);
}
}
@Autowired
private MyAuthenticationService myAuthenticationService;
//这里进行注入,由于需要当成参数,且参数是对应的两个接口类型,所以该类需要实现对应的两个接口,使得当成总体参数
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.successHandler(myAuthenticationService) //登录成功的处理
.failureHandler(myAuthenticationService) //登录失败的处理
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
}
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*自定义登录成功或失败处理器
*/
@Service
public class MyAuthenticationService implements
AuthenticationSuccessHandler,AuthenticationFailureHandler{
//用来得到可以操作响应的response,可以用它来操作重定向(转发好像操作不了)
//虽然单纯的使用response也可以操作,也可以使用request操作转发,但对于ajax来说
//无论重定向还是转发并不会起作用,因为接收者不是浏览器,而是ajax
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private ObjectMapper objectMapper; //可以将map转换成json
/**
* 登录成功后处理的逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Authentication authentication) throws
IOException, ServletException {
System.out.println("登录成功后继续处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/");
//httpServletResponse.sendRedirect("/");
//RequestDispatcher requestDispatcher = httpServletRequest.getRequestDispatcher("/");
//requestDispatcher.forward(httpServletRequest, httpServletResponse);
Map result = new HashMap();
result.put("code", HttpStatus.OK.value()); //对应的value一般返回的是200
result.put("message","登录成功");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
/**
* 登录失败的处理逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AuthenticationException e) throws
IOException, ServletException {
System.out.println("登录失败后进行处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/toLoginPage");
//httpServletResponse.sendRedirect("/toLoginPage");
//RequestDispatcher requestDispatcher = httpServletRequest.getRequestDispatcher("/toLoginPage");
//requestDispatcher.forward(httpServletRequest, httpServletResponse);
Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value()); //对应的value一般返回的是401
result.put("message","登录失败");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
}
//当然,这里你可以先不进行打印数据,而是先操作重定向来进行验证是否跳转
//虽然一般会进行跳转(虽然前面验证过了)
<div style="padding:30px;">
<input type="button" onclick="login()"
class="button button-block bg-main text-big input-big" value="登录">
div>
<script>
//注意:使用这里的ajax会导致验证码不填写也会进行提交(点击框框返回的时候会提示),而没有对应的阻拦
function login(){
$.ajax({
type:"POST", //请求类型
dataType:"json", //服务器返回数据的类型
url:"/login1", //请求路径
data:$("#formLogin").serialize(), //将表单数据转化成json
success:function(data){
//alert(data)
//这里可以先将弹出框进行查看,而不用先执行下面的,即将下面注释掉进行测试,防止你出现了问题
if(data.code == 200){
//因为返回200,就说明了是认证成功,那么就可以直接的访问了
window.location.href = "/";
}else{
alert(data.message)
}
}
})
}
script>
/*
我们先看看对应的两个方法
.successHandler(myAuthenticationService) //登录成功的处理
.failureHandler(myAuthenticationService) //登录失败的处理
点击.successHandler(myAuthenticationService) 进去
public final T successHandler(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
return this.getSelf();
}
看到这个successHandler应该知道,在前面说过,他是操作认证成功的,所以原来的设置是被他覆盖了
点击.failureHandler(myAuthenticationService)进去
public final T failureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return this.getSelf();
}
同样也是如此failureHandler也是覆盖原来的设置,这就是为什么使用我们自己的配置的原因
*/
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.successHandler(myAuthenticationService)
.failureHandler(myAuthenticationService)
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().anyRequest().authenticated();
//通过测试,上面的顺序基本是不能改变的,也就是说,的确会覆盖之前的认证成功和认证失败的结果(因为后执行)
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
<div class="head-l">
<a class="button button-little bg-red" href="/logout">
<span class="icon-power-off">span>退出登录a>div>
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.successHandler(myAuthenticationService)
.failureHandler(myAuthenticationService)
.and().logout().logoutUrl("/out") //设置对应的退出请求为"/out"
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
}
<div class="head-l">
<a class="button button-little bg-red" href="/out">
<span class="icon-power-off">span>退出登录a>
div>
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*自定义登录成功或失败处理器以及退出认证登录处理器
*/
@Service
public class MyAuthenticationService implements
AuthenticationSuccessHandler,AuthenticationFailureHandler, LogoutSuccessHandler {
//用来得到可以操作响应的response,可以用它来操作重定向
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private ObjectMapper objectMapper; //可以将map转换成json
/**
* 登录成功后处理的逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Authentication authentication) throws
IOException, ServletException {
System.out.println("登录成功后继续处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/");
Map result = new HashMap();
result.put("code", HttpStatus.OK.value()); //对应的value一般返回的是200
result.put("message","登录成功");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
/**
* 登录失败的处理逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AuthenticationException e) throws
IOException, ServletException {
System.out.println("登录失败后进行处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/toLoginPage");
Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value()); //对应的value一般返回的是401
result.put("message","登录失败");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Authentication authentication) throws
IOException, ServletException {
System.out.println("退出之后进行处理");
redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/totoLoginPage");
}
}
.successHandler(myAuthenticationService) //登录成功的处理
.failureHandler(myAuthenticationService) //登录失败的处理
.and().logout().logoutUrl("/out")
.logoutSuccessHandler(myAuthenticationService)
//这里进行覆盖原来的退出方法,使用我们自己配置的
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/toLoginPage","/css/**","/images/**","/js/**","/code/**");
//加上了"/code/**"
}
## redis相关配置
# Redis数据库索引(默认为0)
#spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.164.128
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
#spring.redis.password=
#将对应的ip和端口的注释解除即可(端口也可以不用解除,因为默认也是6379)
#对应由于默认0数据库,所以可以不用解除,且我们也并没有操作密码
<img src="/code/image" alt="" width="100" height="32" class="passcode"
style="height:43px;cursor:pointer;"
onclick="this.src=this.src+'?'">
package com.lagou.filter;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证码过滤器 OncePerRequestFilter 一次请求只会经过一次过滤器
*/
@Component
//这里为什么需要继承OncePerRequestFilter,主要是为了操作对应的类,后面会继续说明为什么
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, FilterChain filterChain) throws
ServletException, IOException {
//判断是否是登录请求
//equalsIgnoreCase不考虑大小写
if(httpServletRequest.getRequestURI().equals("/login1")&&
httpServletRequest.getMethod().equalsIgnoreCase("post")){
//imageCode是输入的图形验证码的name
String imageCode = httpServletRequest.getParameter("imageCode");
System.out.println(imageCode);
//具体的验证流程,后面会进行补充
}
//如果不是登录请求直接放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//将我们的验证过滤器加入到用户名密码验证过滤器的前面
//正是因为有这样的操作,使得spring security不止只操作默认的15个过滤器
http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);
//只需要传递Filter接口及其子类即可(两个都是)
//validateCodeFilter父辈中有Filter接口
//第二个UsernamePasswordAuthenticationFilter.class他所在的参数类型是Class extends Filter>)
//所以说两个都是
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.successHandler(myAuthenticationService)
.failureHandler(myAuthenticationService)
.and().logout().logoutUrl("/out")
.logoutSuccessHandler(myAuthenticationService)
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
//http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);
/*
那么可以写在后面吗,答:可以,因为这里只是进行设置,也就是顺序
只要指定了在谁的过滤器前面无论是先执行还是后执行,都会有标记的,使得在前面
那么可以执行两次吗,答可以但先标记的在后标记的前面,所以加载时会出现两个过滤器
只是后标记的会覆盖先标记的,所以只会执行一次的方法验证
*/
//这里我们回到前面的一个问题,为什么要继承OncePerRequestFilter类,而不直接的实现Filter接口呢:
//主要是对应的方便,虽然我们也可以实现Filter接口来进行意一样的操作
//但继承继承对应的OncePerRequestFilter类(他的父辈,有Filter接口)
//方便一些,不用进行强制转换了
//实现Filter接口操作HttpServletRequest和HttpServletResponse需要如下操作:
/*
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
接口的强制,编译期不会识别,因为该接口对应的实现类可能是强制转换的类的子类,虽然编译期一般不识别
//当然直接的类自然会识别,虽然不会识别接口,但运行期自然会识别,不对就会报错
*/
}
package com.lagou.execption;
import org.springframework.security.core.AuthenticationException;
/**
* 验证码异常类
*/
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
package com.lagou.filter;
import com.lagou.controller.ValidateCodeController;
import com.lagou.execption.ValidateCodeException;
import com.lagou.service.impl.MyAuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证码过滤器 OncePerRequestFilter 一次请求只会经过一次过滤器
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
private MyAuthenticationService myAuthenticationService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, FilterChain filterChain) throws
ServletException, IOException {
//判断是否是登录请求
//equalsIgnoreCase不考虑大小写
if(httpServletRequest.getRequestURI().equals("/login1")&&
httpServletRequest.getMethod().equalsIgnoreCase("post")){
//imageCode是输入的图形验证码的name
String imageCode = httpServletRequest.getParameter("imageCode");
System.out.println(imageCode);
//具体的验证流程
try {
validate(httpServletRequest, imageCode);
//只要这个方法出现了异常,就代表,对应的验证码操作失败,自然需要有对应的错误信息
}catch (ValidateCodeException e){
e.printStackTrace();
myAuthenticationService.onAuthenticationFailure(
httpServletRequest,httpServletResponse,e);
return;
}
}
//如果不是登录请求直接放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void validate(HttpServletRequest request,String imageCode){
//由于对应的验证码信息存在redis里面,所以我们需要取出redis的信息进行对比,使得验证成功
//因为过期时间,所以验证码也通常需要在一定的时间里进行验证,这也是redis的一个作用
String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE+"-"+request.getRemoteAddr();
String s = stringRedisTemplate.boundValueOps(redisKey).get();
//实际上也就是操作stringRedisTemplate.opsForValue().get(redisKey);,可以看对应的源码就知道了
//验证码的判断
if(!StringUtils.hasText(imageCode)){
throw new ValidateCodeException("验证码的值不能为空");
}
if(s==null){
throw new ValidateCodeException("验证码已过期");
}
if(!s.equals(imageCode)){
throw new ValidateCodeException("验证码不正确");
}
//从redis中删除验证码信息
stringRedisTemplate.delete(redisKey);
}
}
/**
* 登录失败的处理逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AuthenticationException e) throws
IOException, ServletException {
System.out.println("登录失败后进行处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/toLoginPage");
Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value()); //对应的value一般返回的是401
result.put("message",e.getMessage()); //这里进行了改变
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
#session设置
#配置session超时时间
server.servlet.session.timeout=60
http.sessionManagement() //设置session管理
.invalidSessionUrl("/toLoginPage") //session失效后跳转的页面,默认是自己设置的登录页面
//这里还是设置成对应的/toLoginPage
.maximumSessions(1);//session最大的会话数量
//1:代表同一时间,只能有一个用户可以登录,一般有个专业术语:互踢
//.expiredUrl("/toLoginPage"); //被踢出时,进行跳转的页面
//实际上只要你被踢,那么相当于没有认证,而没有认证,就会重定向到认证页面
//只是对应的默认页面(转发的),是不需要经过认证的,也就是说,他是例外(设置的例外),使得不需要认证也可以访问
//所以不会默认重定向到登录页面,但他也只是操作一次,也就是说,只有第一次是例外,其他时候会进行重定向到认证页面
//我们可以发现,大多数都是重定向
//这也是为了防止前面操作什么路径认证后就会到什么路径的设置的原因,虽然并不会操作
http.sessionManagement() //设置session管理
.invalidSessionUrl("/toLoginPage") //session失效后跳转的页面,默认是自己设置的登录页面
//这里还是设置成对应的/toLoginPage
.maximumSessions(1)//session最大的会话数量
//1:代表同一时间,只能有一个用户可以登录,一般有个专业术语:互踢
.expiredUrl("/toLoginPage"); //被踢出时,进行跳转的页面
//实际上只要你被踢,那么相当于没有认证,而没有认证,就会重定向到认证页面
//只是对应的默认页面(转发的),是不需要经过认证的,也就是说,他是例外(设置的例外),使得不需要认证也可以访问
//所以不会默认重定向到登录页面,但他也只是操作一次,也就是说,只有第一次是例外,其他时候会进行重定向到认证页面
//我们可以发现,大多数都是重定向
//这也是为了防止前面操作什么路径认证后就会到什么路径的设置的原因,虽然并不会操作
http.sessionManagement() //设置session管理
.invalidSessionUrl("/toLoginPage") //session失效后跳转的页面,默认是自己设置的登录页面
//这里还是设置成对应的/toLoginPage
.maximumSessions(1)//session最大的会话数量
//1:代表同一时间,只能有一个用户可以登录,一般有个专业术语:互踢
.expiredUrl("/toLoginPage")//被踢出时,进行跳转的页面实际上只要你被踢
// 那么相当于没有认证,只是对应的默认页面,是不经过认证的,所以不会默认重定向到登录页面
.maxSessionsPreventsLogin(true); //如果达到最大会话数量,就阻止登录
//只要加上了maxSessionsPreventsLogin(true),那么对应的expiredUrl("/toLoginPage")也就没有作用了
//那么这里的顺序可以随便写吗,答:不可以随便写,但部分可以
//因为对应的返回都是一样的(一般表单的配置是通过and分开的,其他的基本可以互换)
//也就是说可以这样:
/*
http.sessionManagement() //设置session管理
.invalidSessionUrl("/toLoginPage") //session失效后跳转的页面,默认是自己设置的登录页面
//这里还是设置成对应的/toLoginPage
.maximumSessions(1)//session最大的会话数量
//1:代表同一时间,只能有一个用户可以登录,一般有个专业术语:互踢
.maxSessionsPreventsLogin(true) //如果达到最大会话数量,就阻止登录
.expiredUrl("/toLoginPage");//被踢出时,进行跳转的页面实际上只要你被踢
// 那么相当于没有认证,只是对应的默认页面,是不经过认证的,所以不会默认重定向到登录页面
*/
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
#使用redis共享session
spring.session.store-type=redis
#好像Spring Security对应会检查库依赖,在没有配置指定使用哪个库时,一般有顺序,通常是redis优先
#所以这个配置可以不用写,但最好写上,防止优先级的判断操作
#且也防止被其他人优先(虽然redis优先级别很高,基本是第一)
//关闭csrf防护
//http.csrf().disable();
//注意:当我们开启防护时,退出登录必须是post请求,且需要携带对应的_csrf值
//否则不会让你退出(一般是404错误),不让你找到
//当然了,也可以关闭防护进行解决,但大多数情况下,我们不会进行关闭,所以解决方式如下:
/*
找到对应的index.html修改如下即可:
将原来的注释了,进行添加表单来操作post了
上面的原来的进行的注释,经过测试,必须是post,若是get,则任然报错404
然而这是对退出登录来说的规定,其他的若是get不会操作这样的防护,如后面的/user/saveOrUpdate
*/
<div class="form-group">
<div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
div>
div>
package com.lagou.controller;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 处理登录业务
*/
@Controller
public class LoginController {
/**
* 跳转登录页面
*
* @return
*/
@RequestMapping("/toLoginPage")
public String toLoginPage(HttpServletRequest request,HttpServletResponse response) {
//下面是重新的生成对应的csrf信息
CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
CsrfToken csrfToken = cookieCsrfTokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = cookieCsrfTokenRepository.generateToken(request);
cookieCsrfTokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//CsrfToken.class.getName()的值是CsrfToken类的地址
//其中csrfToken.getParameterName()的值是_csrf
//而我们操作前端得到的值,也就是csrfToken,虽然有两个方法获取
//一个是CsrfToken类的地址,一个是_csrf,而这里就是操作_csrf
//他们都是String类型,这是自然的,因为参数就是操作String类型的
//这时我们进行了生成信息,继续重新项目,清除缓存,可以发现,没有重定向了
//可以我们却登录不了,因为这是我们自己生成的
//那么这些代码如何来的呢,我们可以找到CsrfFilter过滤器
//在前面说过的15个过滤器中的其中一个,我们关闭对应的防护,基本会使得不会加载该过滤器
/*
我们继续看他的介绍
org.springframework.security.web.csrf.CsrfFilter:
csrf又称跨域请求伪造, SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息
如果不包含,则报错,起到防止csrf攻击的效果
他并没有说明生成的csrf信息,实际上他操作了生成csrf信息放在request里面,具体部分核心代码如下:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName(
可以发现,对应的操作与我们这里生成的操作是一样的
但是,为什么他放在了request里面,却得到的是null呢,通过我的研究,发现,我们一般可以使用
web.ignoring().antMatchers("/toLoginPage","/css/**","/images/**","/js/**","/code/**");
中操作的toLoginPage,代替原来操作的
antMatchers("/toLoginPage").permitAll()
虽然都是放行,但是前面的放行,基本只针对于认证,而不操作csrf防护
对应的防护相当于认证的意思,也就是防护后,后面就不需要防护了
使得csrf防护一直重定向,因为在没有放行的情况下,request中会进行删除,使得为null,然后重定向
如此反复,自然得不到,使得操作null,所以我们需要将
web.ignoring().antMatchers("/toLoginPage","/css/**","/images/**","/js/**","/code/**");
修改成
web.ignoring().antMatchers("/css/**","/images/**","/js/**","/code/**");
原来操作的表单中,的这个部分:
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests()
.anyRequest().authenticated();
修改成
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll()
.anyRequest().authenticated();
*/
return "login";
}
}
//开启csrf防护,可以设置哪些不需要防护
http.csrf().ignoringAntMatchers("/user/saveOrUpdate"); //相当于局部的关闭防护,而不是全部进行关闭
//但也要明白,这是操作是否进行防护的,不需要防护时,基本与生成csrf值并没有关系,也就是说
//只要你需要对应的csrf值,那么如果是不防护的,那么与是否有csrf值没有关系
//也就是说,他使得不操作_csrf值,那么无论是否有_csrf值,也就没有关系了
//在对应的user_add.html里面可以找到post请求的地址
//经过测试,发现post确实需要防护的认证,因为操作get时不会报错
当前页面URL | 被请求页面URL | 是否跨域 | 原因 |
---|---|---|---|
http://www.lagou.com/ | http://www.lagou.com/index.html | 否 | 同源(协议,域名,端口号相同) |
http://www.lagou.com/ | https://www.lagou.com/index.html | 跨域 | 协议不同(http/https) |
http://www.lagou.com/ | http://www.baidu.com/ | 跨域 | 主域名不同(lagou/baidu) |
http://www.lagou.com/ | http://kaiwu.lagou.com/ | 跨域 | 子域名不同(www/kaiwu) |
http://www.lagou.com/ | http://www.lagou.com:8090 | 跨域 | 端口号不同(8080/8090) |
function toCors() {
$.ajax({
// 默认情况下,标准的跨域请求是不会发送cookie的
xhrFields: {
withCredentials: true
},
url: "http://localhost:8090/user/1", // 根据ID查询用户
success: function (data) {
alert("请求成功." + data)
}
});
}
/**
* 跨域配置信息源
*/
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允许跨域的站点,*代表所有站点
corsConfiguration.addAllowedOrigin("*");
//允许跨域的http方法,*代表所有方法,站点可以跨域,但方法却也需要进行设置跨域
corsConfiguration.addAllowedMethod("*");
//允许跨域的请求头,*代表所有请求头,在方法之前,自然也是需要使得可以操作请求头的
corsConfiguration.addAllowedHeader("*");
//允许带凭证
corsConfiguration.setAllowCredentials(true);
//UrlBasedCorsConfigurationSource是实现CorsConfigurationSource的实现类
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource
= new UrlBasedCorsConfigurationSource();
//对所有的url都生效
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",corsConfiguration);
return urlBasedCorsConfigurationSource;
}
//开启跨域支持,操作跨域的信息
http.cors().configurationSource(corsConfigurationSource());
表达式 | 说明 |
---|---|
permitAll | 指定任何人都允许访问 |
denyAll | 指定任何人都不允许访问 |
anonymous | 指定匿名用户允许访问 |
rememberMe | 指定已记住的用户允许访问 |
authenticated | 指定任何经过身份验证的用户都允许访问,不包含anonymous |
fullyAuthenticated | 指定由经过身份验证的用户允许访问,不包含anonymous和rememberMe |
hasRole(role) | 指定需要特定的角色的用户允许访问,会自动在角色前面插入’ROLE_’ |
hasAnyRole([role1,role2]) | 指定需要任意一个角色的用户允许访问,会自动在角色前面插入’ROLE_’ |
hasAuthority(authority) | 指定需要特定的权限的用户允许访问 |
hasAnyAuthority([authority,authority]) | 指定需要任意一个权限的用户允许访问 |
hasIpAddress(ip) | 指定需要特定的IP地址可以访问 |
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll()
.anyRequest().authenticated();
//其中permitAll,也就是指定任何人都允许访问,也就是我们说的都进行放行的意思,可以将访问当成放行的意思
//authenticated,指定任何经过身份验证的用户都允许访问,不包含anonymous
//也就是认证后,可以访问,除了匿名用户
//加上这个相当于多出来了对应的访问放行,即设置/user开头的请求需要ADMIN权限
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
//当然如果删除的话,自然不需要权限就可以访问,这里使得,需要该权限访问,即加了限制
//所以对应的我们进行添加权限,是服务于这个的,若这个没有,无论你是否添加,都可以访问,所以我们也说这个是限制
/*
因为可以这样
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll()
.and().authorizeRequests().antMatchers("/css/**").permitAll()
或者
.and().authorizeRequests().antMatchers("/toLoginPage","/css/**").permitAll()
*/
//点击hasRole进去,可以发现,的确加上了ROLE_,即总体来说对应的是ROLE_ADMIN,注意是对应的
/**
* 根据用户名查询用户
* @param username 前端传入的用户名
* @return
* @throws UsernameNotFoundException
*/
//只有该一个接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库得到信息,具体如何得到的,并不需要知道
//因为我们只需要学习对应的spring security,而不是学习该项目
User byUsername = userService.findByUsername(username);
if(byUsername == null){
throw new UsernameNotFoundException("用户没有找到,"+username);
}
//从这里开下面的进行改变
//权限的集合,前面我们并不需要操作权限,所以设置为空集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
//现在我们需要操作权限了,所以这里进行赋予权限
if(username.equalsIgnoreCase("admin")){
//SimpleGrantedAuthority是GrantedAuthority的实现类
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
//当是admin用户时,给他ROLE_ADMIN(下面的权限管理),由于前面操作"/user/**"时
//需要这个权限,所以我们登录后,只要是该用户,就可以访问
}else{
authorities.add(new SimpleGrantedAuthority("ROLE_PRODUCT"));
}
//到这里结束改变
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
username //用户名
,"{bcrypt}"+byUsername.getPassword()
//密码的加密与否,这里是加密对比的(对比这个),而不是他去加密
,true //用户是否启用,true代表启用,false代表未启用
,true //用户是否过期,true代表没有过期,false代表过期
,true //用户凭证是否过期,true代表没有过期,false代表过期
,true //用户是否锁定,true代表未锁定,false代表锁定
,authorities //权限的集合
);
//根据权限的集合,知道有什么权限,从而,操作可以操作设置的对应的访问
//但我们发现,实际上这里只是根据用户名,给加上权限的,也就是说
//一般是我们自己进行条件设置,哪个用户可以操作什么
return userDetails;
}
/*
如果将http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");变成
http.authorizeRequests().antMatchers("/user/**").hasRole("A");或者
http.authorizeRequests().antMatchers("/user/**").hasRole("a");
可以吗,答:可以,只要你添加的权限集合中,包括了对应的权限即可
如authorities.add(new SimpleGrantedAuthority("ROLE_A"));
authorities.add(new SimpleGrantedAuthority("ROLE_a"));等等
即除了ROLE_是固定的,后面的可以随便写,只要对应了,当然有大小写之分的,即a不是A
所以说,对应的名称基本上是可以随便写的,但一般我们为了更方便的识别,尽量使用见名知意的单词
*/
package com.lagou.handle;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义权限不足的处理
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AccessDeniedException e) throws IOException,
ServletException {
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.getWriter().write("权限不足,联系管理员");
}
}
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
//这里就是操作权限失败后,进行操作的方法,实际上不只是权限,csrf除了登录的操作失败不会到这里来
//其他的失败会到这里来(如前面不防护报403错误的地方,就操作这个页面了,甚至优先于post和get的对比错误)
//即需要传递post,但设置了@GetMapping
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
//设置/product开头的请求,只要是本机,且有ADMIN或者PRODUCT的两个权限的其中一个就可以访问
http.authorizeRequests().antMatchers("/product/**")
.access("hasAnyRole('ADMIN','PRODUCT') and hasIpAddress('127.0.0.1')");
//我们可以发现,要操作多个权限,需要使用access来进行操作,and代表一起,当然还有or或等等
//也可以发现,一般都会加上ADMIN使得该权限也可以操作这个
package com.lagou.service.impl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* 自定义授权类
*/
@Component
public class MyAuthorizationService {
/**
* 检查是否有对应的访问权限,虽然可以不检查而直接返回,但这是不好的习惯
* @param authentication
* @param request
* @param id
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request){
//得到对应的信息,即UserDetails的信息,该User是UserDetails的实现类
User user = (User) authentication.getPrincipal();
//获取用户的权限
Collection<GrantedAuthority> authorities = user.getAuthorities();
// 获取用户名
String username = user.getUsername();
// 如果用户名为admin,则不需要认证
if (username.equalsIgnoreCase("admin")) {
return true;
} else {
// 循环用户的权限,判断是否有ROLE_ADMIN权限,有则返回true
for (GrantedAuthority authority : authorities) {
String role = authority.getAuthority();
if ("ROLE_ADMIN".equals(role)) {
return true;
}
}
}
return false;
}
}
http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");
//会执行实例myAuthorizationService类的check方法,只要返回true,就可以访问
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
/*
假设http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");,简称为直接的对比
和http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");,简称为类对比
一起,会怎么样
答:使用类操作的会进行覆盖,也就是说,他后进行操作,他的结果放行使得成立
所以这时候就算你这样写http.authorizeRequests().antMatchers("/user/**").hasRole("A");
没有对应的权限对比,也会可以访问
最后注意:该类需要扫描的,因为不扫描,又怎么会找到并执行方法呢
如果不加,一般得不到值,也就会报错
注意:对于上面路径的覆盖,有如下的解释
路径>**>*>,相同的则是类优先
所以说,实际上并不是覆盖,而是选择
比如类:/user/**,直接的对比:/user/**,则使用类
若类:/user/aa/**,直接的对比:/user/**,则使用直接的对比,因为路径>**
若类:/user/*,直接的对比:/user/*,则使用类
若类:/user/aa/*,直接的对比:/user/aa/*,则使用类
若类:/user/*,直接的对比:/user/**,则使用直接的对比,因为**>*
所以会有如上的覆盖(实际上是选择,即对应的操作并没有直接对比,或者执行方法等等)情况
那么以此类推,若类:/user/a,直接的对比:/user/a,则使用类,所以说,相同的类优先
回忆:**代表当前路径以及其子路径,*代表当前路径,当然有些使用*也会代表当前路径及其子路径
但在服务器里面,通常情况下*代表当前路径
一般情况下,在不代表路径时,*代表所有的意思
*/
package com.lagou.service.impl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* 自定义授权类
*/
@Component
public class MyAuthorizationService {
/**
* 检查是否有对应的访问权限,虽然可以不检查而直接返回,但这是不好的习惯
* @param authentication
* @param request
* @param id
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request,Integer id){
if (id > 10) {
return false;
}
return true;
}
}
http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");
//修改成如下:
//使用自定义Bean授权,并携带路径参数
http.authorizeRequests().antMatchers("/user/delete/{id}").
access("@myAuthorizationService.check(authentication,request,#id)");
//#id得到路径里面的{id}中的id值,如/user/delete/1,那么#id就是1,到方法里面,也就是1
//使得只要对应路径的id的值大于10,就不能访问
//由于不是**或*的覆盖不了**或*的,所以我们需要注释掉如下:
//http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
//否则还是会删除的,除非他是/user/*,一个*,因为并没有覆盖到,需要对应路径
//部分代码
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启注解支持
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 查询所有用户
*
* @return
*/
@RequestMapping("/findAll")
@PreAuthorize("hasRole('ADMIN')") //访问这个方法需要ADMIN权限,简称为指定
public String findAll(Model model) {
List<User> userList = userService.list();
model.addAttribute("userList", userList);
return "user_list";
}
//@ProAuthorize : 注解适合进入方法前的权限验证
/*
那么有个问题,假设有对应的http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
变成了http.authorizeRequests().antMatchers("/user/**").hasRole("ADMI");
则谁优先呢,答:直接的对比优先
如果配置类里的configure((HttpSecurity http)方法操作了
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMI");
那么选择这个,进行覆盖
当然覆盖与前面的是一样的,指定(注解指定)<直接<类
所以在测试时,记得注释掉对应的直接对比或者类的操作,否则可能就算有ADMIn权限,因为被覆盖了,使得权限还是不足
*/
http.authorizeRequests().antMatchers("/user/findAll").hasRole("ADMIN");
/**
* 用户修改页面跳转
*
* @return
*/
@RequestMapping("/update/{id}")
@PreAuthorize("#id<3") //只有id小于3的可以访问,而不是小于3的不能访问,因为返回true,就代表可以访问
//即针对参数的权限控制
//该id是对应的Integer id的值,而不是{id},因为Integer id的值他基本与{id}对应
//所以当如/update/1,那么该#id就是1
//如果Integer id是null,那么默认的#id<3这个判断是true,即默认是true
public String update(@PathVariable Integer id, Model model) {
User user = userService.getById(id);
model.addAttribute("user", user);
return "user_update";
}
/**
* 根据用户ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
//returnObject:返回参数,这里也就是User
//对应的判断,返回true可以访问,这里就是,只能我自己查询自己的用户,而不能查询其他用户
@PostAuthorize("returnObject.username==authentication.principal.username")
public User getById(@PathVariable Integer id) {
//获取认证的信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication);
System.out.println(RememberMeAuthenticationToken.class.
isAssignableFrom(authentication.getClass()) == true);
//如果返回true,代表这个登录认证的信息,来源于自动登录
if(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass()) == true){
//一般来说,spring security在捕获异常时,会重定向到认证页面,一般并不会打印异常的信息
throw new RememberMeAuthenticationException("认证来源于RememberMe");
}
User user = userService.getById(id);
return user;
}
/*
@PostAuthorize:@PostAuthorize在方法执行后再进行权限验证,适合验证带有返回值的权限
Spring EL提供返回对象能够在表达式语言中获取到返回对象的 returnObject
returnObject:代表return返回的值
*/
/**
* 用户删除-多选删除
*
* @return
*/
@GetMapping("/delByIds")
@PreFilter(filterTarget = "ids",value = "filterObject%2==0") //剔除参数为奇数的值
//规则并不是一定操作权限,也可以操作数据,这里就是操作对应的List ids中剔除值为奇数的值
//实际上也就是保留filterObject%2==0返回true的值
//filterObject代表ids里面集合中的元素,自然是依次判断的
public String delByIds(@RequestParam(value = "id") List<Integer> ids) {
for (Integer id : ids) {
System.out.println(id);
}
return "redirect:/user/findAll";
}
//@PreFilter:可以用来对集合类型的参数进行过滤,将不符合条件的元素剔除集合
/**
* 查询所有用户-返回json数据
*
* @return
*/
@RequestMapping("/findAllTOJson")
@ResponseBody
@PostFilter("filterObject.id%2==0")
//剔除所有奇数的用户信息,我们发现,他操作的是返回值的剔除,而不是方法的剔除
//即后进行剔除,而不是@PreFilter这样的先进行剔除
public List<User> findAllTOJson() {
List<User> userList = userService.list();
System.out.println(userList);
return userList;
}
//@PostFilter:可以用来对集合类型的返回值进行过滤,将不符合条件的元素剔除集合
/*
@ProAuthorize : 注解适合进入方法前的权限验证
@PostAuthorize:@PostAuthorize在方法执行后再进行权限验证,适合验证带有返回值的权限
对应判断得到的为true时,表示可以访问,当然也可以直接的设置true和false,但并不提倡
Spring EL提供返回对象能够在表达式语言中获取到返回对象的returnObject
returnObject:代表return返回的值
@PreFilter:可以用来对集合类型的参数进行过滤,将不符合条件的元素剔除集合,操作参数,先剔除
@PostFilter:可以用来对集合类型的返回值进行过滤,将不符合条件的元素剔除集合,操作返回值,后剔除
*/
package com.lagou.service.impl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* 自定义授权类
*/
@Component
public class MyAuthorizationService {
/**
* 检查是否有对应的访问权限,虽然可以不检查而直接返回,但这是不好的习惯
*
* @param authentication
* @param request
* @param id
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request) {
User principal = (User) authentication.getPrincipal();
Collection<GrantedAuthority> authorities = principal.getAuthorities();
if ("admin".equalsIgnoreCase(principal.getUsername())) {
return true;
} else {
//获取路径
String requestURI = request.getRequestURI();
//当访问user时,需要ADMIN权限才可访问
if (requestURI.contains("/user")) {
for (GrantedAuthority grantedAuthority : authorities) {
if ("ROLE_ADMIN".equals(grantedAuthority.getAuthority())) {
return true;
}
}
}
//当访问product时,需要PRODUCT权限才可访问
if (requestURI.contains("/product")) {
for (GrantedAuthority grantedAuthority : authorities) {
if ("ROLE_PRODUCT".equals(grantedAuthority.getAuthority())) {
return true;
}
}
}
//实际上ADMIN是都可以访问的,只是这里为了突出分开,所以给出不同的结果
/*
就如前面写过的
for (GrantedAuthority authority : authorities) {
String role = authority.getAuthority();
if ("ROLE_ADMIN".equals(role)) {
return true;
}
}
直接这个循环即可,使得不同判断,只要你是ADMIN权限,就可以访问
*/
}
return false;
//当都不满足时,自然是访问不了的
}
}
//修改如下:
//使用自定义Bean授权
http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");
/*
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMI");
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
当上面两个一起操作时,对应的是或者关系还是与关系,还是覆盖关系(后覆盖,和前覆盖)
后覆盖:写在后面的覆盖前面的
前覆盖:写在前面的覆盖后面的
答:是或者关系,相当于
http.authorizeRequests().antMatchers("/user/**")
// .access("hasAnyRole('ADMIN','ADMI')");
*/
package com.lagou.service.impl;
import com.lagou.domain.Permission;
import com.lagou.domain.User;
import com.lagou.mapper.PermissionMapper;
import com.lagou.service.PermissionService;
import com.lagou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
*基于数据库完成认证
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
/**
* 根据用户名查询用户
* @param username 前端传入的用户名
* @return
* @throws UsernameNotFoundException
*/
//只有该一个接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库得到信息,具体如何得到的,并不需要知道
//因为我们只需要学习对应的spring security,而不是学习该项目
User byUsername = userService.findByUsername(username);
if(byUsername == null){
throw new UsernameNotFoundException("用户没有找到,"+username);
}
//权限的集合,前面我们并不需要操作权限,所以设置为空集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
//基于数据库查询用户对应的权限
List<Permission> byUserId = permissionService.findByUserId(byUsername.getId());
for(Permission permission : byUserId){
authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}
//上面使得添加对应用户的权限,由于数据库表的原因,那么用户只能操作对应的权限
//这也使得,不需要我们手动的进行规则操作了,数据库已经操作完毕了
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
username //用户名
,"{bcrypt}"+byUsername.getPassword() //密码
,true //用户是否启用,true代表启用,false代表未启用
,true //用户是否过期,true代表没有过期,false代表过期
,true //用户凭证是否过期,true代表没有过期,false代表过期
,true //用户是否锁定,true代表未锁定,false代表锁定
,authorities //权限的集合
);
//"{noop}"+byUsername.getPassword()代表不使用密码加密
//实际上是{noop}代表了不使用的意思,而该参数正好是密码的参数,所以是不使用密码加密
return userDetails;
}
public static void main(String[] args) {
//使用加密的类,进行加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//得到123456加密后的值
String code = bCryptPasswordEncoder.encode("123456");
System.out.println(code); //$2a$10$qbhLCxg43v7bv0oPk1dNz.vXCL.qT4.kEmMRdTJlh/CFY9v.OwAtq
}
}
//查询数据库所有权限列表
List<Permission> list = permissionService.list();
for(Permission permission:list){
//添加请求权限
http.authorizeRequests().antMatchers(permission.getPermissionUrl()).
hasAnyAuthority(permission.getPermissionTag());
//hasAnyAuthority没有前缀ROLE_
}
//这样就使得,没有用户只能根据数据库来进行操作权限
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
<version>3.0.4.RELEASEversion>
dependency>
DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
/*
判断用户是否已经登陆认证,引号内的参数必须是isAuthenticated()
sec:authorize="isAuthenticated()" 若登录认证了,返回true,没有登录认证,则返回false
true则显示其标签的内容,false则不显示其标签的内容,虽然必须登录认证后才可以访问
所以这个只是为了以防万一的,虽然基本不会操作到不显示(因为基本要认证后才可访问该页面,也才可使得他进行操作)
但这时,也基本是认证的了
获得当前用户的用户名,引号内的参数必须是name
sec:authentication="name" 使得标签的内容为对应的用户名
判断当前用户是否拥有指定的权限,引号内的参数为权限的名称
sec:authorize="hasRole('role')"
若用户满足了该权限,则返回true,即显示标签的内容,否则返回false,即不显示该标签的内容
*/
<div sec:authorize="isAuthenticated()">
<span sec:authentication="name">span>
<img src="images/y.jpg" class="radius-circle rotate-hover" height="50" alt=""/>
div>
<div sec:authorize="hasAuthority('user:findAll')">
<h2><span class="icon-user">span>系统管理h2>
<ul style="display:block">
<li><a href="/user/findAll" target="right"><span class="icon-caret-right">span>
用户管理a>li>
<li><a href="javascript:void(0)" onclick="toCors()" target="right"><span
class="icon-caret-right">span>跨域测试a>li>
ul>
div>
<div sec:authorize="hasAuthority('product:findAll')">
<h2><span class="icon-pencil-square-o">span>数据管理h2>
<ul>
<li><a href="/product/findAll" target="right"><span class="icon-caret-right">span>
商品管理a>li>
ul>
div>
# 安全过滤器自动配置
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableConfigurationProperties({SecurityProperties.class})
@ConditionalOnClass({AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class})
@AutoConfigureAfter({SecurityAutoConfiguration.class})
public class SecurityFilterAutoConfiguration {
//该类加载完后,一般会触发SecurityAutoConfiguration类
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({DefaultAuthenticationEventPublisher.class})
@EnableConfigurationProperties({SecurityProperties.class})
//一般有默认的用户名和密码,虽然上一级的也有这个注解,操作基本也是一样
//但基本这个会覆盖,所以就没有说明上一个的该注解了
@Import({SpringBootWebSecurityConfiguration.class,
WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class})
public class SecurityAutoConfiguration {
//注解一般起到辅助作用,如使用注解的值,或者操作值等等
//部分代码,不同的版本,代码可能不同,但总体来说是一样的
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList();
private boolean passwordGenerated = true;
@Import({SpringBootWebSecurityConfiguration.class,
WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class})
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnBean({WebSecurityConfigurerAdapter.class})
@ConditionalOnMissingBean(
name = {"springSecurityFilterChain"}
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableWebSecurity //该注解开启security安全功能
public class WebSecurityEnablerConfiguration {
public WebSecurityEnablerConfiguration() {
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
boolean debug() default false;
}
//从上面看,主要的类是WebSecurityConfiguration.class,也就是安全的配置类
//该配置类里面就生成了过滤器链
//其中@EnableGlobalAuthentication注解里面的部分内容如下:
/*
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({AuthenticationConfiguration.class})
@Configuration
public @interface EnableGlobalAuthentication {
}
其中@Import({AuthenticationConfiguration.class})中的AuthenticationConfiguration进行了加载
他主要是配置认证信息
*/
@Bean(
name = {"springSecurityFilterChain"}
)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null &&
!this.webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter =
(WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(
new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
return (Filter)this.webSecurity.build();
}
@Autowired
@Qualifier("springSecurityFilterChain")
private Filter filter;
//进行注入,因为过滤器很多,我们需要精确的注入
/**
* 查询所有用户
*
* @return
*/
@RequestMapping("/findAll")
//@PreAuthorize("hasRole('ADMIN')") //访问这个方法需要ADMIN权限
public String findAll(Model model) {
List<User> userList = userService.list();
model.addAttribute("userList", userList);
System.out.println(filter); //这里进行打印
System.out.println(1);
return "user_list";
}
/*
//日志信息
[
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@22b7ef2b, org.springframework.security.web.context.SecurityContextPersistenceFilter@7afe0e67, org.springframework.security.web.header.HeaderWriterFilter@23f60b7d, org.springframework.web.filter.CorsFilter@6d421fe, org.springframework.security.web.csrf.CsrfFilter@5ce3409b, org.springframework.security.web.authentication.logout.LogoutFilter@37d0d373, com.lagou.filter.ValidateCodeFilter@6f5d0190, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4af12a63, org.springframework.security.web.session.ConcurrentSessionFilter@77ccded4, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@31228d83, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@16bbaab3, org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter@72eed547, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3c17bd0b, org.springframework.security.web.session.SessionManagementFilter@4aa3fc9a, org.springframework.security.web.access.ExceptionTranslationFilter@5e37fb82, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6987a133
]
//打印信息
FilterChainProxy[Filter Chains: [[ Ant [pattern='/images/**'], []], [ Ant [pattern='/css/**'], []], [ Ant [pattern='/js/**'], []], [ Ant [pattern='/code/**'], []], [ any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@22b7ef2b, org.springframework.security.web.context.SecurityContextPersistenceFilter@7afe0e67, org.springframework.security.web.header.HeaderWriterFilter@23f60b7d, org.springframework.web.filter.CorsFilter@6d421fe, org.springframework.security.web.csrf.CsrfFilter@5ce3409b, org.springframework.security.web.authentication.logout.LogoutFilter@37d0d373, com.lagou.filter.ValidateCodeFilter@6f5d0190, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4af12a63, org.springframework.security.web.session.ConcurrentSessionFilter@77ccded4, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@31228d83, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@16bbaab3, org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter@72eed547, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3c17bd0b, org.springframework.security.web.session.SessionManagementFilter@4aa3fc9a, org.springframework.security.web.access.ExceptionTranslationFilter@5e37fb82, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6987a133
]]
]
]
*/
@Bean(
name = {"springSecurityFilterChain"}
)
public Filter springSecurityFilterChain() throws Exception {
//下面这一段代码打上断点,因为我们直接的进入容易到接口
//那么就不好直接的查看了,且就算你找到对应的类,但细节的信息,却不好观看
//所以我们使用断点来进行细节的查看并可以忽略找类的细节
//虽然对应的接口只有一个实现类,所以这里主要是观看细节信息
boolean hasConfigurers = this.webSecurityConfigurers != null &&
!this.webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter =
(WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(
new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
return (Filter)this.webSecurity.build();
}
/*
不同的版本,可能代码显示不同,但大致一样
我们调试到
return (Filter)this.webSecurity.build();
进入:
会到这里:
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = this.doBuild();
return this.object;
} else {
throw new AlreadyBuiltException("This object has already been built");
}
}
我们进入this.object = this.doBuild();
后面说明进入时,一般都是进入对应的方法的,可能后面会所有省略,注意即可
然后到这里(没有调试之前,这里是看接口的(但该接口只有一个实现类,所以并不影响手动查找观看):
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
其中this.init();是初始化的方法,我们进去(也就是进入,而不是点击进去,即是调试的进去)
后面说的进去和进入都是调试的进去,(手动的)点击进去或者进入,都是我们去进入,而不是调试的进入
部分代码如下:
private void init() throws Exception {
Collection> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
我们再次的进去Collection> configurers = this.getConfigurers();
出现如下:
private Collection> getConfigurers() {
List> result = new ArrayList();
Iterator var2 = this.configurers.values().iterator();
while(var2.hasNext()) {
List> configs = (List)var2.next();
result.addAll(configs);
}
return result;
}
接下来就是为什么使用调试的原因了,我们看看对应的this.configurers的值
调试时可以查看到,基本直接的找不会给出来,所以这就是为什么使用调试的原因
可以发现,有个类的相关地址名称
你对比一下配置类,可以发现,就是你的配置类的相关地址名称
也就是说,那么可以说,他是将我们配置类的信息放在了result的list集合中,并返回
好像只有该一个信息,因为size=1
接下来继续调试,一直下一步,直到回到上一个方法
即init()方法
部分代码如下:
Collection> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
SecurityConfigurer configurer;
while(var2.hasNext()) {
configurer = (SecurityConfigurer)var2.next();
configurer.init(this);
}
我们进入configurer.init(this);,第一个获得基本上就是我们写的配置类信息
因为基本上对应的size=1,只有他(这里是)
进入后,到如下
public void init(WebSecurity web) throws Exception {
HttpSecurity http = this.getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor =
(FilterSecurityInterceptor)http.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}
我们进入 HttpSecurity http = this.getHttp();
protected final HttpSecurity getHttp() throws Exception {
if (this.http != null) {
return this.http;
} else {
AuthenticationEventPublisher eventPublisher = this.getAuthenticationEventPublisher();
this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
AuthenticationManager authenticationManager = this.authenticationManager();
this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
Map, Object> sharedObjects = this.createSharedObjects();
这里进行了创建
this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder,
sharedObjects);
if (!this.disableDefaults) {
((HttpSecurity)((DefaultLoginPageConfigurer)((HttpSecurity)((HttpSecurity)
((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)
((HttpSecurity)this.http.csrf().and()).addFilter(new
WebAsyncManagerIntegrationFilter()).exceptionHandling()
.and()).headers().and()).sessionManagement()
.and()).securityContext().and()).requestCache()
.and()).anonymous().and()).servletApi().and()).apply(new
DefaultLoginPageConfigurer())).and())
.logout();
ClassLoader classLoader = this.context.getClassLoader();
List defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
Iterator var6 = defaultHttpConfigurers.iterator();
while(var6.hasNext()) {
AbstractHttpConfigurer configurer = (AbstractHttpConfigurer)var6.next();
this.http.apply(configurer);
}
}
this.configure(this.http);
return this.http;
}
}
其中我们创建的
this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
看看对应的http的值,一般我们只需要看对应的两个值:
filters和configurers这两个值,这时他们的size=0,即现在还没有值,继续调试
在上面的代码中,我们可以看到,有csrf(),他代表开启csrf防护,所以的确是默认开启csrf防护的
即前面说过的默认开启csrf防护就是因为这里进行了实现,他操作一个过滤器,放在configurers里面
在15个默认的过滤器中,其中有个org.springframework.security.web.csrf.CsrfFilter,他是生成该过滤器的配置类
一般放在configurers里面的是对应的配置类,用来生成对应的过滤器的,也就是第4个
其中addFilter(new WebAsyncManagerIntegrationFilter())就是添加一个过滤器,放在filters里面
该过滤器也就是15个默认过滤器的第一个,即
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter:
而这个addFilter方法内容如下(可能是一行的,所以我们手动点击进去):
public HttpSecurity addFilter(Filter filter) {
Class extends Filter> filterClass = filter.getClass();
if (!this.comparator.isRegistered(filterClass)) {
throw new IllegalArgumentException("The Filter class " + filterClass.getName() + " does
not have a registered order and cannot be added without a specified order. Consider
using addFilterBefore or addFilterAfter instead.");
} else {
this.filters.add(filter); 这里就进行了添加
return this;
}
}
所以的确是进行了添加
后面的exceptionHandling()也是添加对应的过滤器的配置类,用来创建15个默认过滤器的如下:
org.springframework.security.web.access.ExceptionTranslationFilter,也就是第14个
后面的就不依次说明,可以自己进行尝试
因为这样的说明并没有意义
我们直接的下一步,当调试完后,看看对应的值即可
发现filters和configurers这两个值分别是1和10
我们直接调试到this.configure(this.http);
那么这个this是谁呢,看看前面,我们是进入this.getHttp();里面的,那么这个this是谁呢
再看前面,我们是configurer.init(this);进入的,发现,this就是configurer,由于那么就可以知道
该this是对应的配置类信息,或者说,就是配置类
那么this.configure(this.http);,也就是调用我们写的配置类的该方法,看看是否有对应的方法,不难看出
就是我们前面多次提到的configure(HttpSecurity http)这个方法
所以对应的http再操作时,为什么会使得覆盖等的信息,这就是原因,所以他可以被我们进行配置
为了验证,我们进入,发现,的确是我们写的方法
为了验证对应的过滤器是否添加,我们进去
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
到这里
public HttpSecurity addFilterBefore(Filter filter, Class extends Filter> beforeFilter) {
this.comparator.registerBefore(filter.getClass(), beforeFilter);
return this.addFilter(filter);
}
进入return this.addFilter(filter);,再前面我们知道他是添加过滤器的,所以可以看看方法
public HttpSecurity addFilter(Filter filter) {
Class extends Filter> filterClass = filter.getClass();
if (!this.comparator.isRegistered(filterClass)) {
throw new IllegalArgumentException("The Filter class " + filterClass.getName() + " does
not have a registered order and cannot be added without a specified order. Consider
using addFilterBefore or addFilterAfter instead.");
} else {
this.filters.add(filter);
return this;
}
}
实际上是一样的,我们看看对应的this(这时是http),所以可以看到filters和configurers这两个值
发现filters多出一个过滤器了
调试到 如下
http.authorizeRequests().antMatchers(permission.getPermissionUrl())
.hasAnyAuthority(permission.getPermissionTag());
这里执行后,对应的配置类就会添加一个,从而生成对应的过滤器,一般是最后一个过滤器,即
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
当调试完 http.formLogin()这个后,也会多出一个过滤器配置,从而生成对应的过滤器
调试到 rememberMe()也会多出一个配置类,从而生成对应的过滤器
调试到http.csrf().disable();,若有的话,则会删除对应的配置类
调试到http.cors().configurationSource(corsConfigurationSource());,配置类加一个
在后面,他们这些配置类会变成对应的过滤器的
调试完后,回到
this.configure(this.http);
return this.http;
然后再次回到
public void init(WebSecurity web) throws Exception {
HttpSecurity http = this.getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor =
(FilterSecurityInterceptor)http.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}
先看看web的值,可以发现有个securityFilterChainBuilders,执行后,从size=0会变成size=1
点开他,发现,多出了值,也就是之前的http,当然该http的配置类创建的过滤器名称并不是一定会与过滤器名称对应
或者说,操作的过滤器与他的名称并不一定类似,且可能不会创建
当然可能后续也会进行添加或者删除(虽然基本是默认的),这里要注意
所以他的信息名称也并不是一定与日志的过滤器链的信息名称对应(主要是名称可能不对应)
具体看后面过滤器配置了创建过滤器时,对应的创建的过滤器与其名称对比就知道了
比如:
DefaultLoginPageConfigurer不会创建过滤器(这里是)
FormLoginConfigurer创建UsernamePasswordAuthenticationFilter,后面是他的一些配置
类似的一般是如下:
CsrfConfigurer创建CsrfFilter,
RememberMeConfigurer创建RememberMeAuthenticationFilter,等等当然并没有规则,只是类似而已
当然这些并不需要注意
一直向下调试,回到这里
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
进入this.configure();
private void configure() throws Exception {
Collection> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
while(var2.hasNext()) {
SecurityConfigurer configurer = (SecurityConfigurer)var2.next();
configurer.configure(this);
}
}
也是获取配置类信息,并进行操作,进入 configurer.configure(this);
发现到了我们配置类的如下方法
configure(WebSecurity web)
打开web,可以看到ignoredRequests,在对应的放行操作之前,size=0,操作后
根据放行参数的个数,来决定,比如我这里
web.ignoring().antMatchers("/images/**","/css/**","/js/**","/code/**");
那么对应的size=4,且对应的值是这些参数,从而操作放行或者说不被拦截
继续调试,回到
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
进入O result = this.performBuild();
部分代码如下:
int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
List securityFilterChains = new ArrayList(chainSize);
Iterator var3 = this.ignoredRequests.iterator();
其中chainSize得到放行数和对应的http的个数,加起来是5
然后根据这个5,创建一个list集合
再看后面的部分代码
while(var3.hasNext()) {
RequestMatcher ignoredRequest = (RequestMatcher)var3.next();
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest, new Filter[0]));
}
其中执行四次(因为对应的长度是4),将对应的放行信息放入创建的集合中
在看后面代码:
var3 = this.securityFilterChainBuilders.iterator();
while(var3.hasNext()) {
SecurityBuilder extends SecurityFilterChain> securityFilterChainBuilder =
(SecurityBuilder)var3.next();
securityFilterChains.add(securityFilterChainBuilder.build());
}
这里只会执行一次,因为对应的长度只有1
我们得到的对应的值,就是http,进入
securityFilterChains.add(securityFilterChainBuilder.build());,也就是执行了http的build()方法
进入后,看如下:
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = this.doBuild();
return this.object;
} else {
throw new AlreadyBuiltException("This object has already been built");
}
}
是否有点点熟悉,在前面我们也进入过一个的方法,只是对应的this,不同了
之前的是WebSecurity类型,而现在是HttpSecurity类型
进入this.object = this.doBuild();
代码如下:
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
回到了这里,注意:对应的this由WebSecurity类型变成了HttpSecurity类型
进入this.init();
然后进入
Collection> configurers = this.getConfigurers();
代码如下:
private Collection> getConfigurers() {
List> result = new ArrayList();
Iterator var2 = this.configurers.values().iterator();
while(var2.hasNext()) {
List> configs = (List)var2.next();
result.addAll(configs);
}
return result;
}
之前操作WebSecurity类型时,对应的this.configurers是配置类的地址信息,或者说就是配置类,即size=1
而现在的size=14,我这里是,当然不同的设置,会使得不同的
如前面的是否开启防护,开启少一个,没有开启,则不会少等等
也就是对应的过滤器的配置类的数量
回到上一级:
Collection> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
SecurityConfigurer configurer;
while(var2.hasNext()) {
configurer = (SecurityConfigurer)var2.next();
configurer.init(this);
}
进入configurer.init(this);,由于是14个,那么会执行14次
我们依次的进入,当然对应过滤器配置类做的事情就不依次介绍了
总体来说,就是之前的我们自定义的配置类进行的操作,过滤器配置类也进行一次操作
只是使用的是他们的init方法
这里我们直接跳过,回到上一级
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
进入this.configure();
private void configure() throws Exception {
Collection> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
while(var2.hasNext()) {
SecurityConfigurer configurer = (SecurityConfigurer)var2.next();
configurer.configure(this);
}
}
然后进入 configurer.configure(this);,这里执行了每个过滤器配置类的配置方法,这里并不是操作放行了
先看看第一个配置类的该方法(this是HttpSecurity类型,而不是WebSecurity类型)
我们很容易在对应的方法中可以看到,他首先创建对应的过滤器,不难发现
该过滤器也就是该配置类所需要生成的过滤器,并在最后进行了添加,即执行http.addFilter方法
而正是因为参数是HttpSecurity类型,所以也就添加到了http里面去了,而由于地址的原因
那么WebSecurity类型里面的http的值也是一样的,当然后面的过滤器配置类也是如此
所以过滤器配置类的确进行了创建过滤器
但是对应的顺序基本是依次放在后面的,也就是说,根据配置类的顺序来进行
其中 O result = this.performBuild();里面就是进行了排序,进入
protected DefaultSecurityFilterChain performBuild() {
this.filters.sort(this.comparator);
return new DefaultSecurityFilterChain(this.requestMatcher, this.filters);
}
this.filters.sort(this.comparator);这一步就是进行排序
至此我们可以对照排序前后与之前的15个默认过滤器,可以发现,排序之前是操作过滤器的添加顺序
而排序之后,就是前面的默认15个过滤器的顺序了(基本是一致的)
最后返回时,返回的是对应的http过滤器链的真正信息,从而保存到集合中去
至此,上面的操作正好将我们创建的集合都添加完毕,这就是为什么之前,需要将长度进行相加而创建集合的原因
而该集合,就使得放行和过滤器链进行统一操作了,然后一系列的操作后,我们的初始化就操作完毕
总结:
首先得到对应的过滤器和过滤器的配置类(自定义的配置类可以进行添加和删除)
然后得到对应放行,再次进行所有的配置类统一操作初始化,并排序
从而操作了过滤器链的新结果以及放行的新结果
放行基本不会变化,可能过滤器链会发现变化,基本是受我们的自定义配置的影响
然后过滤器链的信息就得到了,当然是包含了对应的过滤器链的信息,还有其他的信息
具体看前面注入的打印,而不是日志的打印(取的是注入打印的过滤器链信息的一部分)
实际上注入的信息是一个代理对应集合的类(调试时可以知道),也就是返回代理该集合的类
那么自然除了过滤器链信息,也包含了其他的信息,因为放行的也在原来的集合
至此过滤器链的生成大致说明完毕,即的确返回了过滤器链的信息
虽然打印结果还有其他信息,基本是放行(拦截)的信息
*/
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse
response) throws AuthenticationException {
//我们在下面一行这里打上断点
if (this.postOnly && !request.getMethod().equals("POST")) { //判断是否是post
throw new AuthenticationServiceException("Authentication method not supported: " +
request.getMethod());
} else {
String username = this.obtainUsername(request); //得到用户名
String password = this.obtainPassword(request); //得到密码
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/*
重启项目,清除缓存
我们现在随便输入用户名和密码,进行认证,那么就会跳转到上面设置的断点
我们可以先看判断,若不是post,那么就会报错,而报错就会重定向到认证页面,可以自己试一下
得到用户名和密码的操作如下:
进入 String username = this.obtainUsername(request);
可以看到如下return request.getParameter(this.usernameParameter);
进入String password = this.obtainPassword(request);
可以看到如下return request.getParameter(this.passwordParameter);
不难发现,的确是获取我们输入的用户名和密码
进入UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
出现如下:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal; 赋值用户名
this.credentials = credentials; 赋值密码
this.setAuthenticated(false);
设置认证结果,false代表现在还是未认证的状态,true则代表已经认证的状态
}
回到上一级
找到 UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
其中this.getAuthenticationManager()得到的是AuthenticationManager接口的变量
但对象是实现他的类(后面有分为对应的实现他的父子类)
我们进入return this.getAuthenticationManager().authenticate(authRequest);
部分代码如下:
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
Class extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
其他的代码就不写上了
我们可以找到
Iterator var8 = this.getProviders().iterator();
其中进入this.getProviders()
public List getProviders() {
return this.providers;
}
看后面的图片,看其内容
*/
/*
我们在后面找到provider.supports(toTest)进入,这是一个判断
第一个
public boolean supports(Class> authentication) {
return AnonymousAuthenticationToken.class.isAssignableFrom(authentication);
}
其中authentication是UsernamePasswordAuthenticationToken的类型变量执行的getClass();
进行比较,对应的类的supports就是比较是否是UsernamePasswordAuthenticationToken.class
很明显不是
第二个
public boolean supports(Class> authentication) {
return RememberMeAuthenticationToken.class.isAssignableFrom(authentication);
}
可以发现,他们都不是,也就是都是false,而不是true
所以会调试到这里
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {
} catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}
进入 result = parentResult = this.parent.authenticate(authentication);
我们发现还是当前的方法,只是对于的this,变了
一般是原来的指向的父类,也是实现了对应的AuthenticationManager接口
我们看看父类的this.getProviders()
进入后,有如下
public List getProviders() {
return this.providers;
}
对应的图片结果:
*/
/*
继续找到后面的provider.supports(toTest)进入,这是一个判断
因为this.getProviders()中的this不同,所以结果也就不同,因为继承,若操作子类,自然可以操作父类,前提没有覆盖
但通常会覆盖,所以这里会使用子类的版本,这时我们指向父类,那么我们使用父类的版本,自然对应的值也就不同的
无论是变量还是方法都基本如此,这是java基础内容
发现是如下
public boolean supports(Class> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
至此,返回true,因为是UsernamePasswordAuthenticationToken.class
往后面调试,可以到 result = provider.authenticate(authentication);
使用对应的provider操作authenticate方法
由于并不是对应的父类或者子类的执行,所以不是当前的方法了
部分的代码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported");
});
找到
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" :
authentication.getName();
可以发现,执行了UsernamePasswordAuthenticationToken的getPrincipal()方法
进入得
public Object getPrincipal() {
return this.principal;
}
在之前说明过,他被赋值了,这里我因为登录的是admin,所以该值就是admin,得到用户名
通过authentication.getName();得到,实际上该authentication.getName();底层也是操作getPrincipal()得到
继续调试,可以找到
尝试从缓存中获取
UserDetails user = this.userCache.getUserFromCache(username);
他代表是否登录过,很明显,现在我们并没有登录(没有缓存),所以user是null
登陆过的话,这里应该是有值的,后面的检查自然也有值,且会使得登录成功
通常都是能够登录成功的,因为我们通常没有修改值的操作
继续调试,可以到如下:
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
他一般是用来检查我们的username的
我们进入后,有如下
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken
authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null,
which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
我们进去UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
在这之前,我们可以看看this.getUserDetailsService()的值,也就是父类的值
我们可以发现,值是我们自己编写的MyUserDetailsService类,也就是对比的数据
所以说他就是调用我们的MyUserDetailsService类的loadUserByUsername方法
由于前面说过,username一般是需要我们自己来进行判断的,所以该方法里面就使用了我们的自己判断
所以UserDetails loadedUser得到的就是对比的密码数据,换言之,就是对应的对比数据
继续调试,回到上一级
可以知道,上面说的是否登录的数据,被赋值了
我们调试到如下:
this.preAuthenticationChecks.check(user);
很明显,这是检查,一般是认证前(对比前)检查user状态(user也就是UserDetails的对比信息)
我们进入
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is
locked");
throw new
LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
} else if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is
disabled");
throw new DisabledException(
AbstractUserDetailsAuthenticationProvider.this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
} else if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is
expired");
throw new AccountExpiredException(
AbstractUserDetailsAuthenticationProvider.this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
可以发现,对应的检查都完毕,但并不是密码的检查,继续调试
找到
this.additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken)authentication);
这一步非常关键,因为是密码的对比,但却没有用户名的对比
所以也更加确定了对应的用户名需要我们自己来进行判断
所以在前面操作时,对应用户名的地方
基本是可以随便写的(没有因为判断发生错误的情况下,使得重定向,带后缀的,错误的后缀,前面说明过)
我们进入,有如下
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
其中 String presentedPassword = authentication.getCredentials().toString();获取密码
在后面的if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {中
我们进去matches方法,看看他做了什么
到如下
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return this.getPasswordEncoder().matches(rawPassword, encodedPassword);
}
继续进去matches方法
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
} else {
String id = this.extractId(prefixEncodedPassword); id代表加密的方式
得到对应的加密方式对象
PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches.matches(rawPassword,
prefixEncodedPassword);
} else {
String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
}
}
继续进去return delegate.matches(rawPassword, encodedPassword);这里
注意,由于delegate的值是根据对应的是否加密规则(方式)生成的
所以使用noop或者使用bcrypt操作的matches方法不同
其中rawPassword是密码,encodedPassword是对比的密码(即数据库的密码)
如果使用noop,那么进入如下:
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
很明显是直接的比较
而使用bcrypt,那么进入如下:
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
进入return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
这里就是真正的检查,我们输入的密码与数据库的密码进行加密的对比
至于如何的对比,就不说明了,无非基本就是三种情况
输入的加密后对比,数据库的解密后对比,根据他们两个来生成对应的数据库的一样的密码(算法导致)对比
通常情况下,是加密后对比,即输入的加密后进行对比
我们直接下一步,直到回到之前的
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
发现返回是是true,那么取反就是false
至此,只要是对比成功的,那么就不会报错,也就不会重定向(加后缀的)
直接继续调试(下一步),会到如下:
this.postAuthenticationChecks.check(user);
进入后
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account
credentials have expired");
throw new CredentialsExpiredException(
AbstractUserDetailsAuthenticationProvider.this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials
have expired"));
}
}
也是一个检查,但这是认证后(相当于认证后了,因为密码已经对比了,后面的基本会使得认证)
所以也称为认证后,或者对比后
检查user状态,前面的一个类似的是认证前的检查
即this.preAuthenticationChecks.check(user);
而不是this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
进入到 return this.createSuccessAuthentication(principalToReturn, authentication, user);
protected Authentication createSuccessAuthentication(Object principal, Authentication
authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null &&
this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
其中,使用父类的方法时,this还是自己的而不是父类的,如这里使用super,但this却是子类的,而不是父类的
因为是在子类里进行的操作,也就是说,相当于任然是子类调用(虽然看起来是父类的调用)
但他的调用却也是自己(因为是一体了)
只是super用来指定是否操作父类版本的而已,实际上还是相当于this(可以操作父类的this)
因为一般this是操作本类的
}
进入return super.createSuccessAuthentication(principal, authentication, user);
protected Authentication createSuccessAuthentication(Object principal, Authentication
authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new
UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(),
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
this.authoritiesMapper.mapAuthorities(user.getAuthorities()),相当于返回参数里的值,所以也就是
user.getAuthorities()得到权限信息
我们看到他又创建了UsernamePasswordAuthenticationToken,只是与之前不同的是,这里多了一个参数
我们进入看看
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection extends GrantedAuthority> authorities) {
super(authorities); 添加的权限信息,即对比信息中的权限信息
this.principal = principal; 得到的对比信息
this.credentials = credentials;
密码(原来的没有认证的UsernamePasswordAuthenticationToken得到的)
super.setAuthenticated(true);
}
与之前的对比,可以发现,super.setAuthenticated(true);是true,代表已经认证了
继续调试
直到回到 result = provider.authenticate(authentication);
再次调试
可以发现这个
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
他使得我们认证的UsernamePasswordAuthenticationToken的密码设置为null,因为我们已经认证了
那么为了防止数据的泄露,就设置为null(虽然总体来说进行了两次赋值),但结果都是赋值null,影响不大
再次调试,后面会进行返回
至此得到了一个新的UsernamePasswordAuthenticationToken,最后会进行返回
继续往后面走,回到 result = parentResult = this.parent.authenticate(authentication);
也就是说,最终的新的UsernamePasswordAuthenticationToken,还是回到了之前子类里面的那个地方
继续下一步,发现到了这里
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());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
也就是说,我们传递一个没有认证的UsernamePasswordAuthenticationToken得到一个认证的
UsernamePasswordAuthenticationToken
这就相当于我们输入用户名和密码后,认证成功
我们继续下一步,会到如下:
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
父类的方法,当前过滤器的父类,因为前面说过,是父类在执行该方法的,我们进去后
发现,this是子类的引用,那么也就说明前面的解释并没有错误
至此authResult得到的也就是认证的UsernamePasswordAuthenticationToken
当然对应的信息设置(true),是设置他的父类的,这里我们直接简称为
认证的UsernamePasswordAuthenticationToken
而false简称为没有认证的UsernamePasswordAuthenticationToken
继续下一步,到这里
session策略验证,这里并不需要查看
this.sessionStrategy.onAuthentication(authResult, request, response);
继续下一步,到这里
成功身份验证的操作
this.successfulAuthentication(request, response, chain, authResult);
我们进入后,有如下
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse
response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult,
this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
这里有个关键的代码,SecurityContextHolder.getContext().setAuthentication(authResult);
传入认证的UsernamePasswordAuthenticationToken
将认证信息给SecurityContextHolder,所以我们才能得到对应的信息,如对比信息
再次下一步:
this.rememberMeServices.loginSuccess(request, response, authResult);
他一般是操作"记住我"的,或者说有没有"记住我",这里我们并没有操作,我们继续下一步
找到如下
this.successHandler.onAuthenticationSuccess(request, response, authResult);
其中this.successHandler就是我们写的MyAuthenticationService类
在没有写对应配置之前,会使用默认的,前面说过了
在写了之后,这里是写的,即要知道,在过滤器链中我们早就进行了设置
所以而正是因为this是对应的UsernamePasswordAuthenticationFilter
所以我们在执行完http.formLogin()这个后
添加的创建该UsernamePasswordAuthenticationFilter过滤器的配置类后
其中后面的设置也就是为了以后创建过滤器而操作的
至此,会进入我们写的MyAuthenticationService类的onAuthenticationSuccess方法
至此认证流程源码解释完毕,以后我们重新登录时,一般会将得到的返回值看看是否有认证成功,而进行是否重新认证
当然认证失败后,一般是根据异常使得的,而出现了异常,那么对应的基本就不会是true了,所以会操作失败的方法
那么失败的源码分析也就不说明了,总不能所有的代码都进行说明吧,这是不可能的(因为代码太多)
只说明主要部分
总结:首先经历一系列的操作后,使得密码判断,且成功后,给出对应的已经认证的对象
最后保存,然后调用登录成功方法
*/
/*
我们通过登录认证的源码中,可以知道有个"记住我"的操作,接下来我们回到如下:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+
authResult);
}
//在下面这一行打上断开,进行测试
SecurityContextHolder.getContext().setAuthentication(authResult);
如果没有开启记住我,那么这个方法什么都没有做,因为this不同了
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult,
this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
上面打上断点后,我们重启项目,清除缓存,登录认证,并点击记住我
接下来我们不跳过this.rememberMeServices.loginSuccess(request, response, authResult);这里了,直接进入
如下:
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
其中 if (!this.rememberMeRequested(request, this.parameter)) {
进行判断是否携带remember-me(默认是这个),但是若你在前面配置类设置了别名,那么该值就是你设置的
因为在添加的过滤器配置类时,也进行了设置,那么在创建对应的过滤器时
会操作该设置的别名,在调试时,可以知道
我们进入if (!this.rememberMeRequested(request, this.parameter)) {
到如下:
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
request的对象的方法在其多个父类中的一个中,大概是因为注解或者其他的原因,使得this变成对应的父类调用
String paramValue = request.getParameter(parameter);
if (paramValue != null && (paramValue.equalsIgnoreCase("true") ||
paramValue.equalsIgnoreCase("on") ||
paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
return true;
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Did not send remember-me cookie (principal did not set
parameter '" + parameter + "')");
}
return false;
}
}
}
很明显,这里是直接从前端获得对应名称的值并进行判断
注意是判断,因为对应的对象并不是操作得到值的
只是底层会得到值,并进行判断
String paramValue = request.getParameter(parameter);
其中底层会根据该参数名称来进行从前端获取值
所以这就是为什么当我们设置后,对应的前端的name也要一致的原因
因为不一致,那么通过该参数来得到值时,一般返回的是null
若是一样的,在取出前端返回的值,前面我们设置的是true,当成返回值,从后面可以看到
只要是true,no,1,yes,则会返回true,使得后续进行执行
也说明了对应的值的确是多样的(可以回到前面"记住我"的说明,看看是否一样进行确认)
这时可以进行测试,看看对应的名称是否需要一致或者不需要,虽然前面说明过
注意一下:在有互踢的情况下,且到达最大会话数量时
一般操作不了记住我,因为这时,会默认重定向到认证页面(没有后缀)
测试后,当然你也可以不用测试
继续调试(下一步)
回到上一级
调试到如下:
this.onLoginSuccess(request, response, successfulAuthentication);
我们进入后,有如下
注意:当你是持久化的化,那么就是这个方法,因为对应的this的结果会根据持久化来进行改变
所以持久化和不持久化的操作,该方法不同,因为this不同,换言之
就是之前的this.rememberMeServices.loginSuccess(request, response, authResult);
中的this.rememberMeServices不同,可能底层进行了判断
有操作对应的方法,即这个.tokenRepository(getpersistentTokenRepository())
加上这个即操作持久化,前面说明过了
这里根据持久化为主,因为大多数都是操作持久化的
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
获取对应的名称
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
生成一个cookie信息
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username,
this.generateSeriesData(), this.generateTokenData(), new Date());
try {
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
我们进入 this.tokenRepository.createNewToken(persistentToken);
public void createNewToken(PersistentRememberMeToken token) {
this.getJdbcTemplate().update(this.insertTokenSql, new Object[]{token.getUsername(),
token.getSeries(), token.getTokenValue(), token.getDate()});
}
很明显,对应的this.insertTokenSql是sql语句信息(点击进去看看就可以知道)
而后面的是对应的用户名,以及前面说的
series:登录序列号,随机生成策略,用户输入用户名和密码登录时
该值重新生成,使用remember-me功能,该值保持不变
和
token:随机生成策略,每次访问都会重新生成
和
expiryTime:token过期时间
实际上这里的this,就是我们持久化方法的返回值
调试时可以知道,与方法的返回类型一样,该值的生成在启动时,就生成了,可能是操作对应的过滤器链时进行调用生成的
实际上就是一个值,而不是类似值
后面就是加到数据库里面的信息,这就是信息的来源
至于为什么会是返回值,这里并不说明了,因为总不能所有都要说明吧
接下来打开数据库,现在我们直接下一步,执行完后,到 this.addCookie(persistentToken, request, response);
再看数据库,会发现多出一条数据,即的确是生成的信息
再看 this.addCookie(persistentToken, request, response); ,根据名称,很容易发现,是给浏览器的cookie
那么对应的操作是否与添加到数据库的信息是一样的呢,我们进入后,有如下
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request,
HttpServletResponse response) {
this.setCookie(new String[]{token.getSeries(), token.getTokenValue()},
this.getTokenValiditySeconds(), request, response);
}
传递了series:登录序列号,随机生成策略,用户输入用户名和密码登录时
该值重新生成,使用remember-me功能,该值保持不变
和
token:随机生成策略,每次访问都会重新生成
其中this.getTokenValiditySeconds()是基本固定的数据,1209600
是否看这个有点眼熟,没错,就是之前配置类里面的设置的失效时间
通过修改再次测试,可以发现,值改变了,实际上他的改变是过滤器配置类操作过滤器时,进行的设置
可以点开那个配置里,就可以知道了
进入setCookie方法
protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request,
HttpServletResponse response) {
String cookieValue = this.encodeCookie(tokens); 这里通过series和token一起生成了cookie的值
以后的解析也就是这个,然后再根据数据库的用户名和他们再次解析的密码进行对比,然后认证,当然这是后话了
this.cookieName一般默认是remember-me,与设置的名称没有关系,所以基本是remember-me不变
Cookie cookie = new Cookie(this.cookieName, cookieValue);
cookie.setMaxAge(maxAge); 设置过期时间
cookie.setPath(this.getCookiePath(request));
if (this.cookieDomain != null) {
cookie.setDomain(this.cookieDomain);
}
if (maxAge < 1) {
cookie.setVersion(1);
}
if (this.useSecureCookie == null) {
cookie.setSecure(request.isSecure());
} else {
cookie.setSecure(this.useSecureCookie);
}
cookie.setHttpOnly(true);
response.addCookie(cookie); 这里就是将cookie给浏览器了
}
那么继续下一步,我们知道流程已经走完,我们直接的点击下一个断点(不是下一个,具体可以看34章博客)
使得不操作调试,但可以继续调试(虽然下一个也可,但通常需要一直到底才行,所以基本操作不了,可能也会退出)
记住之前的String cookieValue = this.encodeCookie(tokens);这个值
这时我们看看浏览器的cookie,发现的确有对应的cookie了,且名称是remember-me,值也是这个值
接下来我们退出浏览器,看看他是如何操作
我们到RememberMeAuthenticationFilter类里面,找到如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws
IOException, ServletException {
在这里打上断点
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder populated with remember-me token:
'" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext().getAuthentication(),
this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response,
rememberMeAuth);
return;
}
} catch (AuthenticationException var8) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me
token, as AuthenticationManager rejected Authentication returned by
RememberMeServices: '" +
rememberMeAuth + "'; invalidating remember-me token", var8);
}
this.rememberMeServices.loginFail(request, response);
this.onUnsuccessfulAuthentication(request, response, var8);
}
}
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as it
already contained: '" + SecurityContextHolder.getContext().getAuthentication() +
"'");
}
chain.doFilter(request, response);
}
}
重新访问,那么会到上面的断点
一直下一步,会到
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
我们进入后
部分如下:
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response)
{
String rememberMeCookie = this.extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
} else {
this.logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
this.logger.debug("Cookie was empty");
this.cancelCookie(request, response);
return null;
} else {
UserDetails user = null;
try {
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
user = this.processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
return this.createSuccessfulAuthentication(request, user);
} catch (CookieTheftException var6) {
this.cancelCookie(request, response);
throw var6;
} catch (UsernameNotFoundException var7) {
this.logger.debug("Remember-me login was valid but corresponding user not
found.", var7);
} catch (InvalidCookieException var8) {
this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
} catch (AccountStatusException var9) {
this.logger.debug("Invalid UserDetails: " + var9.getMessage());
} catch (RememberMeAuthenticationException var10) {
this.logger.debug(var10.getMessage());
}
this.cancelCookie(request, response);
return null;
}
}
}
其中 String rememberMeCookie = this.extractRememberMeCookie(request);会从浏览器获得cookie
继续调试,会到String[] cookieTokens = this.decodeCookie(rememberMeCookie);
这里就进行了解密,也可以顺便对比一下数据库的数据,发现的确一样的
我们再次下一步,进入 user = this.processAutoLoginCookie(cookieTokens, request, response);
会看到这两个
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
得到对应的信息
再次调试,进入
PersistentRememberMeToken token =
this.tokenRepository.getTokenForSeries(presentedSeries);
找到如下
return (PersistentRememberMeToken)this.getJdbcTemplate().queryForObject(this.tokensBySeriesSql,
(rs, rowNum) -> {
return new PersistentRememberMeToken(rs.getString(1), rs.getString(2),
rs.getString(3), rs.getTimestamp(4));
}, new Object[]{seriesId});
解释:就是从数据库里查找对应的序列号,也就是presentedSeries,即series
至此很显然,我们的数据库有值,可以查询出来
继续下一步,会到
PersistentRememberMeToken token =
this.tokenRepository.getTokenForSeries(presentedSeries);
看看token的值,会发现,是我们查询的值
继续调试
直到这里
这里重新生成一个token信息,我们也会发现,只有token和时间会变化
PersistentRememberMeToken newToken = new
PersistentRememberMeToken(token.getUsername(), token.getSeries(),
this.generateTokenData(), new Date());
执行后,可以看看对应的newToken值
try {
再次操作配置类的数据返回值,操作数据库语句,进行更新
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
newToken.getDate());
再次的发送cookie,注意:你可能会认为,对应的生成的cookie不同,实际上是相同的
因为单独的话,的确不同,但是结合序列号来说,则会是相同的
大概是算法的原因,所以前面说过,token不唯一
好处是:理论上不同的token可以登录多个地方,在安全保证的情况下,我们提高了扩展性
this.addCookie(newToken, request, response);
继续调试,可以到
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
注意,并不是之前的我们的认证操作,因为this不同
我们进入后
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (this.delegate != null) {
return this.delegate.loadUserByUsername(username);
} else {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
Iterator var3 = this.delegateBuilders.iterator();
while(var3.hasNext()) {
AuthenticationManagerBuilder delegateBuilder =
(AuthenticationManagerBuilder)var3.next();
this.delegate = delegateBuilder.getDefaultUserDetailsService();
这个this.delegate得到的就是对比信息,即我们的MyUserDetailsService
if (this.delegate != null) {
break;
}
}
if (this.delegate == null) {
throw new IllegalStateException("UserDetailsService is required.");
}
this.delegateBuilders = null;
}
}
return this.delegate.loadUserByUsername(username);
所以这里就是返回我们的MyUserDetailsService类中方法的返回值,也就是认证的信息了
}
}
所以之前的return this.getUserDetailsService().loadUserByUsername(token.getUsername());
也就是通过用户名来得到对应的对比信息
继续调试,回到user = this.processAutoLoginCookie(cookieTokens, request, response);
下一步,找到this.userDetailsChecker.check(user);,进入后
发现是对应的检查,好像与之前的一样,只是之前的是分开检查的,分为认证前和认证后,现在是一次性检查
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
throw new
LockedException(this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User
account is locked"));
} else if (!user.isEnabled()) {
throw new
DisabledException(this.messages.getMessage("AccountStatusUserDetailsChecker.disabled",
"User is disabled"));
} else if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(this.messages.getMessage(
"AccountStatusUserDetailsChecker.expired", "User account has expired"));
} else if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(this.messages.getMessage(
"AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired"));
}
}
发现的确如此
至此,那么我们得到之前的对比信息了
继续下一步,我们到 return this.createSuccessfulAuthentication(request, user);
传递了对比信息(可以简称为对比类)
进入后
protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails
user) {
RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, user,
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
auth.setDetails(this.authenticationDetailsSource.buildDetails(request));
这个与前面的认证一样,并不需要在意
return auth;
}
进入 RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, user,
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
参数分别是,固定的生成key值,对比信息,权限信息
public RememberMeAuthenticationToken(String key, Object principal, Collection extends
GrantedAuthority> authorities) {
super(authorities); 设置权限
if (key != null && !"".equals(key) && principal != null && !"".equals(principal)) {
this.keyHash = key.hashCode(); 这里不是密码,而是随机的key值
this.principal = principal; 对比信息
this.setAuthenticated(true);
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
总体来说,我们可以看到对应的this.setAuthenticated(true);,不难知道,他操作了对应的认证,由于对应的设置是在
AbstractAuthenticationToken类里面
我们知道他的子类中有RememberMeAuthenticationToken和UsernamePasswordAuthenticationToken
很明显,由于前面的UsernamePasswordAuthenticationToken操作的是AbstractAuthenticationToken类的值
使得认证,那么这里也操作,即同样的也是认证,至此,认证成功
那么继续往下走,直到回到
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
所以我们得到的rememberMeAuth就是前面的RememberMeAuthenticationToken
继续下一步,到
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
我们看看该值this.authenticationManager,发现他的类型是AuthenticationManager
这个类型我们在前面的认证流程看到过,操作父子类时
我们进入后
回到了熟悉的地方
但是这里有个地方不同了
进入 if (provider.supports(toTest)) {
因为现在的toTest是RememberMeAuthenticationToken的getClass()
而不是UsernamePasswordAuthenticationToken.getClass()
所以不会执行父类的操作,那么是一样的对象吗,答:是的,是一样的对象,可能是都赋值了
所以第一次的判断,就执行往后走
因为如下:
第一个
public boolean supports(Class> authentication) {
return AnonymousAuthenticationToken.class.isAssignableFrom(authentication);
}
第二个
public boolean supports(Class> authentication) {
return RememberMeAuthenticationToken.class.isAssignableFrom(authentication);
}
可以发现,第二个是,所以说所以第一次的判断,就执行往后走
继续调试
到 result = provider.authenticate(authentication);
那么很明显,与认证的provider值不同,所以方法一般也是不同的,我们进入后
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!this.supports(authentication.getClass())) {
return null;
} else if (this.key.hashCode() !=
((RememberMeAuthenticationToken)authentication).getKeyHash()) {
throw new BadCredentialsException(this.messages.getMessage(
"RememberMeAuthenticationProvider.incorrectKey", "The presented
RememberMeAuthenticationToken does not contain the expected key"));
} else {
return authentication;
}
}
直接调试完,这里一般是检查,返回参数,即认证的RememberMeAuthenticationToken
继续走,返回的任然是RememberMeAuthenticationToken,回到
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
也就是说,我们只是进行检查,实际上因为我们已经是认证成功的,所以我们并不需要有创建认证的操作,直接返回即可
但却也要检查一下,认证时,操作密码为null时,一般也检查了
注意:其中的
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
并不是操作密码为null,而是其他操作,这里就不做说明,提醒一下,因为result不同了
继续调试,到SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
发现也进行了保存
我们继续调试,会发现有个
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
他并没有做什么,继续下一步,发现,进行放行了,即到chain.doFilter(request, response);
至此,代码解析完成
总结:与认证一样,需要先使得认证,然后保存,既然认证了,那么自然可登录
只是获取方式不同,前面认证操作是通过
UsernamePasswordAuthenticationToken
而这里是RememberMeAuthenticationToken,但都是操作父类的值为true
使得变成认证,也就是说,只要你让父类的参数为true,就是认证成功
而中间的操作就是一系列的判断等等操作,或者说true就是认证的开关
而中间的就是防止不能随便的打开开关
但是你会发现,并没有与对比进行比对,实际上在更加底层的操作中进行的
这里就不说明了,只给出部分代码
当然,上面的解析,只是说明重要父部分,而很多的细节,并没有说明到
比如,如果删除浏览器cookie会怎么样,删除数据库的对应信息会怎么样等等操作
当然,也不可能所有的细节都说出来,那么肯定不是一朝一夕的都说明的
实际上前面的学习中的文字部分,就有说明,只是没有源码补充而已
*/
/*
使用ctrl+n查找,然后再该类里面找到如下:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain
filterChain) throws ServletException, IOException {
在下面一行这里打上断点
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " +
UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new
MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new
InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
filterChain.doFilter(request, response);
}
}
}
首先我们访问初始页面,即登录页面,那么就会到上面的断点
下一步,到CsrfToken csrfToken = this.tokenRepository.loadToken(request);
一般来说,只要当页面取值时,才会进行获取this.tokenRepository
但是若页面并没有取值,那么也会获取,只是,会推迟(当然也会按照顺序)
这样使得session节省开支,因为不会立即占用session
进入后
public CsrfToken loadToken(HttpServletRequest request) {
return this.delegate.loadToken(request);
}
再次进入return this.delegate.loadToken(request);
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session == null ? null : (CsrfToken)session.getAttribute(this.sessionAttributeName);
}
由于是第一次,那么对应的session肯定是null,所以也返回null
即回到CsrfToken csrfToken = this.tokenRepository.loadToken(request);
即csrfToken值是null
继续调试,到 csrfToken = this.tokenRepository.generateToken(request);
我们进入
public CsrfToken generateToken(HttpServletRequest request) {
return this.wrap(request, this.delegate.generateToken(request));
这个this.delegate的值一般是HttpSessionCsrfTokenRepository对象
}
我们再次进入return this.wrap(request, this.delegate.generateToken(request));
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
}
对应的this.headerName值是:X-CSRF-TOKEN
this.parameterName值是:_csrf
后面的
this.createNewToken()是操作UUID的
private String createNewToken() {
return UUID.randomUUID().toString();
}
回到上一级,进入return this.wrap(request, this.delegate.generateToken(request));中的wrap方法
private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
HttpServletResponse response = this.getResponse(request);
return new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response,
token);
}
进入如下:
return new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response,
token);
SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, HttpServletRequest request,
HttpServletResponse response, CsrfToken delegate) {
this.tokenRepository = tokenRepository;
this.request = request;
this.response = response;
this.delegate = delegate;
}
这里进行构造,继续下一步:
直到回到 csrfToken = this.tokenRepository.generateToken(request);
即csrfToken值变成了
new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response,
token);
我们继续下一步:
到如下this.tokenRepository.saveToken(csrfToken, request, response);
进入后
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse
response) {
if (token == null) {
this.delegate.saveToken(token, request, response);
}
}
因为token不是null,所以跳过
继续下一步,我们可以看到如下:
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
其中
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
将对应的csrfToken值,进行保存,我们看看看对应的
CsrfToken.class.getName()值:CsrfToken类的地址
csrfToken.getParameterName()值:_csrf
他们都是String类型,这是自然的,因为参数就是操作String类型的
继续调试
到 if (!this.requireCsrfProtectionMatcher.matches(request)) {
他就是来判断是否是post请求的
我们进入
public boolean matches(HttpServletRequest request) {
Iterator var2 = this.requestMatchers.iterator();
RequestMatcher matcher;
do {
if (!var2.hasNext()) {
this.logger.debug("All requestMatchers returned true");
return true;
}
matcher = (RequestMatcher)var2.next();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Trying to match using " + matcher);
}
} while(matcher.matches(request));
this.logger.debug("Did not match");
return false;
}
我们调试到} while(matcher.matches(request));
进入后
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
看看this.allowedMethods值,如图所示:
*/
/*
回到这里
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
由于返回false,取反,变成true,所以会执行 filterChain.doFilter(request, response);
也就是放行了,至此,我们访问成功,到达登录页面
现在,在页面上,我们也知道
他操作了_csrf.parameterName和_csrf.token,回顾88章博客spring boot中的Thymeleaf
对应的对象.变量,操作的是变量首字母大写的方法,无论是否有该变量,即只操作方法
那么接下来我们找到csrfToken值,也就是
new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response,
token);
也就是SaveOnAccessCsrfToken类的信息
可以看到这两个方法
public String getParameterName() {
return this.delegate.getParameterName();
}
public String getToken() {
this.saveTokenIfNecessary();
return this.delegate.getToken();
}
很明显,对应的this.delegate值操作的是前面的token,也就是
new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
对应的this.headerName值是:X-CSRF-TOKEN
this.parameterName值是:_csrf
后面的
this.createNewToken()是操作UUID的
private String createNewToken() {
return UUID.randomUUID().toString();
}
DefaultCsrfToken类的代码:
public DefaultCsrfToken(String headerName, String parameterName, String token) {
Assert.hasLength(headerName, "headerName cannot be null or empty");
Assert.hasLength(parameterName, "parameterName cannot be null or empty");
Assert.hasLength(token, "token cannot be null or empty");
this.headerName = headerName;
this.parameterName = parameterName;
this.token = token;
}
public String getHeaderName() {
return this.headerName;
}
public String getParameterName() {
return this.parameterName;
}
public String getToken() {
return this.token;
}
明显知道得到了
this.parameterName值是:_csrf,即
和
UUID.randomUUID().toString();
即,调用的方法,最终的返回是this.delegate操作的
_csrf.parameterName值是:_csrf,也就是this.parameterName,也就是
_csrf.token值是:UUID.randomUUID().toString(),也就是this.token,也就是
我们找到他
public String getToken() {
这里打上断点调试 this.saveTokenIfNecessary();
return this.delegate.getToken();
}
进入 this.saveTokenIfNecessary();
private void saveTokenIfNecessary() {
if (this.tokenRepository != null) {
synchronized(this) {
if (this.tokenRepository != null) {
this.tokenRepository.saveToken(this.delegate, this.request, this.response);
this.tokenRepository = null;
this.request = null;
this.response = null;
}
}
}
}
进入this.tokenRepository.saveToken(this.delegate, this.request, this.response);进入
到达this.tokenRepository的子类HttpSessionCsrfTokenRepository
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse
response) {
HttpSession session;
if (token == null) {
session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
} else {
session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
我们明显的发现,他将token存在session里面了,也就是说
他不只是将对应的token给request里面,也放在session里面,只是session的是对应的this.delegate
且是在取值时进行存放
注意:在页面还没调试完时,会到这个调试
且后面可能需要多次的下一个断点进行跳过,跳过即可,可能其他程序造成的结果
接下来,我们发送登录请求(之前的都进行下一个断点跳过)
至此,我们又回到了(部分代码)
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
这里打了断点的 request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
接下来,我们进入 CsrfToken csrfToken = this.tokenRepository.loadToken(request);
public CsrfToken loadToken(HttpServletRequest request) {
return this.delegate.loadToken(request);
}
进入return this.delegate.loadToken(request);
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session == null ? null : (CsrfToken)session.getAttribute(this.sessionAttributeName);
}
在前面,由于我们是第一次,所以会返回null,但现在我们已经有对应的session了,所以返回对应的数据
一直调试到如下
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
很明显,由于是post,那么这里就会返回true,取反,不放行
调试到如下:
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
获取了页面的csrfToken.getParameterName()值,也就是_csrf的值
在页面是,对应的_csrf值,就是生成的token值
继续调试,到
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " +
UrlUtils.buildFullRequestUrl(request));
}
很明显,是比较session和前端的token值,是否相同,当然,这是相同的,所以取反,则为false
而正是因为从session取值,所以csrfToken.getToken()并不会操作对应的保存session的方法,即
public String getToken() {
this.saveTokenIfNecessary();
return this.delegate.getToken();
}
而是
public String getToken() {
return this.token;
}
因为session保存的是DefaultCsrfToken类对象,而request保存的是SaveOnAccessCsrfToken类对象
但是SaveOnAccessCsrfToken对象
最终的返回值却还是DefaultCsrfToken的对象的getToken,只是多出了一个this.saveTokenIfNecessary();
进行将DefaultCsrfToken类对象保存到session里面的方法,即会发现,一个页面若没有获取csrf的token值时
则会在操作post时,帮我们进行保存,因为这时是request的token
简单来说,就是页面没有操作保存的,我们post帮页面干
继续下一步,那么到 filterChain.doFilter(request, response);,即放行了
那么,csrf认证完毕,但为了更加的追踪,我们找到之前的
session策略验证,之前这里并不需要查看,现在我们来看一看
this.sessionStrategy.onAuthentication(authResult, request, response);
进入后
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) throws SessionAuthenticationException {
SessionAuthenticationStrategy delegate;
for(Iterator var4 = this.delegateStrategies.iterator(); var4.hasNext();
delegate.onAuthentication(authentication, request, response)) {
delegate = (SessionAuthenticationStrategy)var4.next();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Delegating to " + delegate);
}
}
}
其中this.delegateStrategies值有两个,在开启csrf时,就是两个,否则是一个
如图所示:
*/
/*
到这里, for(Iterator var4 = this.delegateStrategies.iterator(); var4.hasNext();
delegate.onAuthentication(authentication, request, response)) {
我们进入 delegate.onAuthentication(authentication, request, response)
注意是第二个循环值的进入
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) throws SessionAuthenticationException {
boolean containsToken = this.csrfTokenRepository.loadToken(request) != null;
if (containsToken) {
this.csrfTokenRepository.saveToken((CsrfToken)null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(newToken, request, response);
request.setAttribute(CsrfToken.class.getName(), newToken);
request.setAttribute(newToken.getParameterName(), newToken);
}
}
注意:随着时间的推移,就算是相同的版本,但对应的代码可能是改变的,因为总会有变化
但操作的结果还是一样的,虽然代码过程变了,比如1+1=2,3-1=2,结果都是2,但过程变了
之所以说明这一点,是因为,你操作我这个版本时,可能代码不同,但仔细的观察,实际上结果是一样的,比如说这样
如图所示:
*/
/*
我们进入(说进入的都是调试到,然后进入,注意即可)
this.csrfTokenRepository.saveToken((CsrfToken)null, request, response);
如下:
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
if (token == null) {
this.delegate.saveToken(token, request, response);
}
}
再次进入 this.delegate.saveToken(token, request, response);
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response)
{
HttpSession session;
if (token == null) {
session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
} else {
session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
我们发现,他得到我们的session,然后并移除掉,因为我们已经用完了,而之所以使用session
是为了可以得到,因为request需要设置,但得到后,那么也就不需要session对应值了
我们回到这里
this.csrfTokenRepository.saveToken((CsrfToken)null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(newToken, request, response);
request.setAttribute(CsrfToken.class.getName(), newToken);
request.setAttribute(newToken.getParameterName(), newToken);
很明显,他又生成了一个newToken,放在reuqest里面使得,当我们再次得到对应的数据时
就会保存到session,也就是说,他是为了给下一个页面做准备,实际上每次的访问,在没有session的情况下
都会有新的token出现,然后存在request里面,给页面做准备,当页面有使用时
会进行保存到session,使得一致性,当然,每次的使用都会进行删除,节省空间
这就是为什么,认证后,对应的token的值发生了改变的原因,因为session没有了,所以需要重新的生成
主要代码如下:
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
那么解析到这里,现在说明一个,放行的原因,即不防护,如前面的代码
http.csrf().ignoringAntMatchers("/user/saveOrUpdate");
我们继续回到CsrfFilter类,在这一行打上断点
request.setAttribute(HttpServletResponse.class.getName(), response);
继续调试
到添加用户这里,如图:
*/
/*
点击提交,查看调试
部分代码如下:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
这里是调试的地方 request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
很明显,由于session删除的,且没有使用对应的csrf值,那么自然的是生成一个新的csrf的token
但也要注意:若对应的页面有获得该值,那么自然也会存放在session里面
比如这里的退出登录,而由于我们又没有操作,那么自然session会存在,这里注意一下即可
因为在以后的操作中,会发现,对应的token与与某一个页面的token是一样的,这就是原因
而不一样的,自然是退出登录或者认证导致的,只要没有使得删除session的操作
那么以后再得到值时,基本token是一样的,当然,由于元素的原因,基本不会变
这里一般会使得有操作查看源代码访问的,之前说明过了
但是退出登录自然也会删除session(虽然这里并没有解析源代码说明,但与登录认证的操作类似)
可能对应的退出登录会触发两次调试
一个是退出,一个的指向,然后到访问
当然这里我们注意即可
而由于他又是post请求,那么会到如下代码:
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " +
UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new
MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new
InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
filterChain.doFilter(request, response);
}
}
if (!this.requireCsrfProtectionMatcher.matches(request)) {
一般的post请求会使得这个判断结果是false,返回true,取反得到false
但是如果你加了不防护,也就是http.csrf().ignoringAntMatchers("/user/saveOrUpdate");
那么无论是什么请求,都返回false,取反得true,即必定会放行
之前的我们的认证时操作的post请求是有对应的值的,但这里
actualToken = request.getParameter(csrfToken.getParameterName());
得不到值,也就是null,这也使得 if (!csrfToken.getToken().equals(actualToken)) {
在进行判断时,取反为true,也就是不会到放行了,且是必定的
因为无论你的csrfToken有没有是session得到的,对该结果都是一样
不会发行,只是若没有操作session,那么这里会进行保存对应的token到session里面
简单来说,就是页面没有操作保存的,我们post帮页面干,当然,若没有操作成功
自然该session会一直存在,操作成功,则会删除,就如登录和退出登录一样
只要成功了就删除session,否则不会删除
但这里若是放行,即他并没有操作保存,但也可以称为删除,虽然并不是
继续调试
到如下:
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new
MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new
InvalidCsrfTokenException(csrfToken, actualToken));
}
很明显会到 this.accessDeniedHandler.handle(request, response, new
MissingCsrfTokenException(actualToken));
在没有设置权限不足的页面下,他就是默认的错误页面,也就是403错误,前面说过了
在设置了http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
指定方法,则操作该方法,前面说过了
也就是说this.accessDeniedHandler值
可以被http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
进行设置,否则使用默认的
我们进入http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
public ExceptionHandlingConfigurer accessDeniedHandler(AccessDeniedHandler
accessDeniedHandler) {
this.accessDeniedHandler = accessDeniedHandler;
return this;
}
发现的确如此
至此,我们的csrf防护解析完毕
*/
/*
现在,我们先重启项目,清除缓存,登录后,在如下地方打上断点
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
IOException, ServletException {
在下面一行这里打上断点
FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
我们可以看看fi的值,可以发现,他后面显示url的值(也就是可以看到/user/findAll)
在他的toString里面可以看到显示
在对应对象后面的值(是该变量的对象),显示的就是其toString方法的返回值,注意即可
接下来,我们点击用户管理,那么会到
FilterInvocation fi = new FilterInvocation(request, response, chain);
创建了一个对象,我们往下走
进入this.invoke(fi);
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if (fi.getRequest() != null && fi.getRequest().getAttribute("
__spring_security_filterSecurityInterceptor_filterApplied") != null &&
this.observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
if (fi.getRequest() != null && this.observeOncePerRequest) {
fi.getRequest().setAttribute("
__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
调试到上面的 InterceptorStatusToken token = super.beforeInvocation(fi);
进入后
找到如下:
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
boolean debug = this.logger.isDebugEnabled();
if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object " +
object.getClass().getName() + " but AbstractSecurityInterceptor only configured to
support secure objects of type: " + this.getSecureObjectClass());
} else {
Collection attributes =
this.obtainSecurityMetadataSource().getAttributes(object);
if (attributes != null && !attributes.isEmpty()) {
if (debug) {
到这里
Collection attributes =
this.obtainSecurityMetadataSource().getAttributes(object);
我们进入后
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
看看他的值,如图所示:
*/
/*
//查询数据库所有权限列表
List list = permissionService.list();
for(Permission permission:list){
//添加请求权限
http.authorizeRequests().antMatchers(permission.getPermissionUrl())
.hasAnyAuthority(permission.getPermissionTag());
}
这里的权限规则操作
那么对应的 Collection attributes =
this.obtainSecurityMetadataSource().getAttributes(object);
中this.obtainSecurityMetadataSource()得到的就是保存了我们设置的权限信息
我们继续进入getAttributes(object);
object就是之前创建的 FilterInvocation fi = new FilterInvocation(request, response, chain);
进入后
public Collection getAttributes(Object object) {
HttpServletRequest request = ((FilterInvocation)object).getRequest();
Iterator var3 = this.requestMap.entrySet().iterator();
Entry entry;
do {
if (!var3.hasNext()) {
return null;
}
entry = (Entry)var3.next();
} while(!((RequestMatcher)entry.getKey()).matches(request));
return (Collection)entry.getValue();
}
我们可以发现,他进行了循环取出,并操作了
} while(!((RequestMatcher)entry.getKey()).matches(request));
我们进入matches方法,看看他做了什么
public boolean matches(HttpServletRequest request) {
if (this.httpMethod != null && StringUtils.hasText(request.getMethod()) && this.httpMethod !=
valueOf(request.getMethod())) {
if (logger.isDebugEnabled()) {
logger.debug("Request '" + request.getMethod() + " " + this.getRequestPath(request) +
"' doesn't match '" + this.httpMethod + " " + this.pattern + "'");
}
return false;
} else if (this.pattern.equals("/**")) {
if (logger.isDebugEnabled()) {
logger.debug("Request '" + this.getRequestPath(request) + "' matched by universal
pattern '/**'");
}
return true;
} else {
String url = this.getRequestPath(request);
if (logger.isDebugEnabled()) {
logger.debug("Checking match of request : '" + url + "'; against '" + this.pattern +
"'");
}
return this.matcher.matches(url);
}
}
我们可以找到 String url = this.getRequestPath(request);
返回了request对应的请求
由于request是之前FilterInvocation fi = new FilterInvocation(request, response, chain);得到的
所以实际上该url就是对应的我们访问的请求
然后调试到return this.matcher.matches(url);
因为this是对应的循环的,而操作this.matcher,就是得到循环的url,与上面的我们访问的url进行对比
那么返回true,由于 } while(!((RequestMatcher)entry.getKey()).matches(request));
进行了取反,即返回false,跳出循环
调试到 return (Collection)entry.getValue();
也就是得到对应的循环的key对应的value
也就是如下图所示:
*/
/*
也就是说,我们得到了配置类中,添加的权限信息结果(匹配当前访问的权限信息)
Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);
即给attributes赋值了
这样也就是得到了,要访问这个路径的权限信息(即系统权限),配置类里面操作的规则权限,称为系统权限
使得后面当你有该权限时,那么就可以访问
继续调试
到这里 Authentication authenticated = this.authenticateIfRequired();
我们进入this.authenticateIfRequired();
如下:
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Previously Authenticated: " + authentication);
}
return authentication;
} else {
authentication = this.authenticationManager.authenticate(authentication);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Successfully Authenticated: " + authentication);
}
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
}
在认证解析中,我们直到有这个 SecurityContextHolder.getContext().setAuthentication(authResult);
将对应的认证成功(设置为true的)的 UsernamePasswordAuthenticationToken
这里Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
再次获得该信息,我们直到,他里面有对应的权限信息的,这里就进行操作
而该权限信息就是当前用户的信息,后端操作数据库的代码中进行的
继续调试,发现,但会该认证的UsernamePasswordAuthenticationToken
也就是说Authentication authenticated = this.authenticateIfRequired();中
authenticated得到认证成功的UsernamePasswordAuthenticationToken
继续下一步,到这里
this.accessDecisionManager.decide(authenticated, object, attributes);
我们看看 this.accessDecisionManager这个值(类型是AccessDecisionManager)
如图所示:
*/
/*
我们进入 this.accessDecisionManager.decide(authenticated, object, attributes);里面的decide方法
传递了参数,分别是:
认证的信息,创建的FilterInvocation类信息,匹配FilterInvocation类地址的权限信息(这个是规定)
public void decide(Authentication authentication, Object object, Collection
configAttributes) throws AccessDeniedException {
int deny = 0;
Iterator var5 = this.getDecisionVoters().iterator();
while(var5.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
int result = voter.vote(authentication, object, configAttributes);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Voter: " + voter + ", returned: " + result);
}
switch(result) {
case -1:
++deny;
break;
case 1:
return;
}
}
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("
AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else {
this.checkAllowIfAllAbstainDecisions();
}
}
我们进入Iterator var5 = this.getDecisionVoters().iterator();
public List> getDecisionVoters() {
return this.decisionVoters;
}
返回一个投票者
继续调试到如下:
int result = voter.vote(authentication, object, configAttributes);
里面的代码中就不说明了,其中有个关键字,assert
一般作用是,只要他后面的值是true,继续下一步,否则抛出异常,当然,这里并不需要理会
返回的结果中,result值的不同,代表不同的投票结果
0:投票者表示弃权
-1:投票者表示不通过
1:投票者表示不通过
我们也发现,他传递了三个参数,在前面说过了,即整体来说
是看看我们用户的权限,是否与匹配的权限(通过将该权限称为系统权限,即配置类里面操作的规则权限,称为系统权限)
是否有相同的,相同,那么一般是返回1,这就是匹配权限的原因,很明显这里是返回1,因为是匹配(匹配成功)的
继续调试
到这里
case 1:
return;
}
进行了返回,即结束的该方法
至此,该 this.accessDecisionManager.decide(authenticated, object, attributes);操作结束
即认证成功,由于没有报错,那么自然的不会到规定的异常页面,即使得放行
虽然说,报错会使得重定向到认证页面(加后缀),但那是操作普通的报错的
即大多数的报错是这样(security在认证时报错,因为对应值是null,使得重定向,基本是这样)
但有些报错是操作自己的(不在认证时不会),注意即可,即重定向报错,基本是操作认证时的
我们点击下一个断点,然后看页面,发现,显示的内容
上面是正常的用户,能够匹配上的,那么如果匹配不上呢,解释如下:
先将页面上操作权限管理的标签去掉,显示对应的内容
去掉sec:authorize="hasAuthority('product:findAll')"
在页面操作权限不会匹配的访问
点击商品管理(这里使用zhaoyang测试,操作的就是用户管理,所以应该不能匹配)
在这之前,我们任然在这里打上断点
public void doFilter(ServletRequest request, ServletResponse response, FilterChain
chain) throws IOException, ServletException {
这里打上断点 FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
在后面的循环中,如图所示:
*/
- 1
- 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
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
![在这里插入图片描述](https://i.1000bd.com/contentImg/2024/03/29/a4fbe0f6048bcaaa.png)
操作的是"/product/findAll"
/*
我们直接到,投票者,那么,看看匹配结果
找到(即调试到,简称为找到) this.accessDecisionManager.decide(authenticated, object, attributes);
进入后
到这里
public void decide(Authentication authentication, Object object, Collection
configAttributes) throws AccessDeniedException {
int deny = 0;
Iterator var5 = this.getDecisionVoters().iterator();
while(var5.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
int result = voter.vote(authentication, object, configAttributes);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Voter: " + voter + ", returned: " + result);
}
switch(result) {
case -1:
++deny;
break;
case 1:
return;
}
}
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("
AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else {
this.checkAllowIfAllAbstainDecisions();
}
}
这时对应的result是-1了,也就是不通过,之所以匹配不成功,是因为在用户的权限里面
没有对应的系统权限,所以匹配不成功
而之前的匹配成功,是因为我们的用户zhaoyang权限如下:
*/
- 1
- 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
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
![在这里插入图片描述](https://i.1000bd.com/contentImg/2024/03/29/04d16ba6ad8da3ed.png)
所以前面的"/user/findAll"可以匹配成功,而"/product/findAll"不会匹配成功
/*
那么到如下:
switch(result) {
case -1:
++deny;
break;
case 1:
return;
}
}
很明显,这里操作break,即结束循环,到这里
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("
AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else {
this.checkAllowIfAllAbstainDecisions();
}
不难直到,会执行
throw new AccessDeniedException(this.messages.getMessage("
AbstractAccessDecisionManager.accessDenied", "Access is denied"));
即抛出异常,那么我们继续调试,到如下:
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException var7) {
this.publishEvent(new AuthorizationFailureEvent(object, attributes,
authenticated, var7));
throw var7;
}
很显然,操作了这些代码:
this.publishEvent(new AuthorizationFailureEvent(object, attributes,
authenticated, var7));
throw var7; 这里又进行抛出异常
继续调试
到如下:
} catch (Exception var10) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
这里得到AuthenticationException(认证)的异常,很明显得不到
RuntimeException ase =
(AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(
AuthenticationException.class, causeChain);
if (ase == null) {
再次得到,看看是否是AccessDeniedException(授权)的异常,很明显是的,即得到返回值
ase = (AccessDeniedException)this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
最后调试到如下:
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because
the response is already committed.", var10);
}
this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
我们进入 this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse
response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
this.logger.debug("Authentication exception occurred; redirecting to authentication
entry
point", exception);
this.sendStartAuthentication(request, response, chain,
(AuthenticationException)exception);
} else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!this.authenticationTrustResolver.isAnonymous(authentication) &&
!this.authenticationTrustResolver.isRememberMe(authentication)) {
this.logger.debug("Access is denied (user is not anonymous); delegating to
AccessDeniedHandler", exception);
this.accessDeniedHandler.handle(request, response,
(AccessDeniedException)exception);
} else {
this.logger.debug("Access is denied (user is " +
(this.authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not
fully authenticated") + ");
redirecting to authentication entry point", exception);
this.sendStartAuthentication(request, response, chain, new
InsufficientAuthenticationException(this.messages.getMessage("
ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
}
}
调试到如下
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
获取了对应的认证成功的信息
继续往下走,到这里
this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
不难知道,这就是异常操作的地方,即我们前面的默认,若设置了话,则操作设置的
至此,我们进行到下一个断点使得结束,查看页面,发现,的确如此,即后面都进行了放行
像这样结束的,并给看页面的,那么最终后面基本都是放行,所以就不说明了
为什么说放行呢,实际上过滤器在没有放行时,也是直接与servlet,一样,操作返回的responst
所以说也是一种放行
那么上面的操作中,都是我们的访问与数据库得到的系统权限的访问,都会匹配成功,在与我们用户的权限操作匹配
如果,我们的访问与数据库得到的系统权限的访问匹配失败,会怎么样,在前面说明过
没有在设置规则的情况下,基本是不操作权限的,这里我们结合代码来观察
将前端的访问和对应的后端的"/product/findAll"修改成"/product/findAl"
继续调试,在如下
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
这里打上断点 FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
不多说,直接到循环那里看看
如图:
*/
- 1
- 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
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
![在这里插入图片描述](https://i.1000bd.com/contentImg/2024/03/29/b8b5aa1789f8c3b4.png)
即操作默认存在的这个
这时我们看对应的result返回值时,发现result既然不是-1,而是1,即可以放行,那么这是为什么呢,实际上这是规则的原因
默认的这个进行匹配时,那么会使得默认匹配成功,所以说,在系统的设置,是为了加上规则
而若加上的权限规则与访问的不同,那么自然也就是没有规则,即放行
至此Spring Security源码大致介绍完成