Spring Boot Security

基本使用

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

基本原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

重点看三个过滤器:
FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部

ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常

UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。

过滤器如何进行加载的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.使用SpringSeurity配置过滤器DelegatingFilterProxy
public class DelegatingFilterProxy extends GenericFilterBean {
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//FilterChainProxy
String targetBeanName = this.getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig());
}

return delegate;
}
}
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
public class FilterChainProxy extends GenericFilterBean {
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);

//获取所有的过滤器
List<Filter> filters = getFilters(fwRequest);

if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}

fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
//执行过滤器链调度
vfc.doFilter(fwRequest, fwResponse);
}
}

UserDetailsService

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。

如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

返回值 UserDetails,这个类是系统默认的用户“主体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface UserDetails extends Serializable {
// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();
}

User类是UserDetails的实现类,以后我们只需要使用 User 这个实体类即可!

1
2
3
4
5
6
7
package org.springframework.security.core.userdetails;
public class User implements UserDetails, CredentialsContainer {
public User(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
}

username表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无

法接收。

PasswordEncoder

1
2
3
4
5
6
7
8
9
10
public interface PasswordEncoder {
// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
1
2
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test01(){
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 对密码进行加密
String midkuro = bCryptPasswordEncoder.encode("midkuro");
// 打印加密之后的数据
System.out.println("加密之后数据:\t"+midkuro);
//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("midkuro", midkuro);
// 打印比较结果
System.out.println("比较结果:\t"+result);
}

登录设定

配置文件

第一种方式:通过配置文件配置固定的账号密码进行登录。

在没配置其账号密码时,默认的账号是user,密码会在控制台中输出随机密码

1
Using generated security password: 6e86c6e9-d661-41ae-aabc-bea8817c4f7b
1
2
spring.security.user.name=111
spring.security.user.password=111

配置类

第二种方式:通过配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//对密码进行加密配置
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
//通过内存添加用户的信息,并赋予登录角色:admin
auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
}

//需要向容器注入加密组件
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}

自定义编写实现类

创建配置类,设置使用哪个UserDetailsService实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}

编写实现类,通过传入的userName查数据库并返回User对象,User对象有用户名密码和操作权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

//mybatis的Mapper
@Autowired
private UsersMapper usersMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//调用usersMapper方法,根据用户名查询数据库
QueryWrapper<Users> wrapper = new QueryWrapper();
// where username=?
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
//判断
if(users == null) {//数据库没有用户名,认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
//从查询数据库返回users对象,得到用户名和密码,返回
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}
1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
private Integer id;
private String username;
private String password;
}

自定义登录页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {

http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/test/index").permitAll() //登录成功之后,跳转路径

//自定义表单的key值,默认是username和password
//.usernameParameter("user")
//.passwordParameter("pwd")


.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
.and().csrf().disable(); //关闭csrf防护
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/test")
public class TestController {

@RequestMapping("hello")
public String hello() {
return "hello security";
}

@RequestMapping("index")
public String index() {
return "hello index";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<!-- 需要添加
<html xmlns:th="http://www.thymeleaf.org">
这样在后面的th标签就不会报错
-->
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>xx</title>
</head>
<body>

<h1>表单提交</h1>
<!-- 表单提交用户信息,注意字段的设置,username 和 password 不能更改 -->
<form action="/user/login" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
<input type="text" name="username" />
<input type="text" name="password" />
<input type="submit" />
</form>
</body>
</html>

用户授权

1.在配置类设置当前访问地址有哪些权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests()
//1 hasAuthority方法
//当前登录用户,只有具有admins权限(并集)才可以访问这个路径
//如果当前的主题具有指定的权限,则返回true,否则返回false,没有权限403
.antMatchers("/test/index1").hasAuthority("admins")

//2 hasAnyAuthority方法(交集)
//如果当前的主体有任何提供的权限(给定的作为一个逗号分隔的字符串列表)的话,返回
true..antMatchers("/test/index2").hasAnyAuthority("admins,manager")

//3 hasRole方法 ROLE_sale
//如果用户具备给定角色就允许访问,否则出现 403。如果当前主体具有指定的角色,则返回 true
.antMatchers("/test/index3").hasRole("sale")

//4.hasAnyRole方法 表示用户具备任何一个角色就可以访问
.antMatchers("/test/index4").hasAnyRole("sale");
}
1
2
3
最长匹配原则(has more characters)说明:

URL请求/app/dir/file.jsp,现在存在两个路径匹配模式/**/*.jsp和/app/dir/*.jsp,那么会根据模式/app/dir/*.jsp来匹配

2.在UserDetailsService,设置返回的User对象的权限

1
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale");

值得注意的是,这里用户添加角色时,名称是Role_sale,原因在于Security会在配置类中的角色名称中自动添加前缀,权限和角色底层校验逻辑一致,而Authority并没有。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static String hasAuthority(String authority) {
return "hasAuthority('" + authority + "')";
}

private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
} else {
//将我们定义的角色名称进行 "ROLE_" 前缀拼接
return "hasRole('ROLE_" + role + "')";
}
}

hasRole 和 hasAuthority 写代码时前缀不同,但是最终执行是一样的,功能上没什么区别;设计上来说,role 和 authority 这是两个层面的权限设计思路,一个是角色,一个是权限,角色是权限的集合。

自定义403页面

1
2
3
4
5
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
}

认证授权注解

使用注解先要开启注解功能!

1
2
3
4
5
6
@Configuration
@EnableWebSecurity
//开启全局注解并设置注解 @Secured、@PreAuthorize生效
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecuritySecureConfig extends WebSecurityConfigurerAdapter {
}

@Secured

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“,必须要有ROLE_sale或者ROLE_manager的角色。

1
2
3
4
5
@GetMapping("update")
@Secured({"ROLE_sale","ROLE_manager"})
public String update() {
return "hello update";
}

@PreAuthorize

该注解适合进入方法前的权限验证,可以将登录用户的 roles/permissions 参数传到方法中。

1
2
3
4
5
6
7
@GetMapping("update")
//同时需包含ROLE_sale和ROLE_manager角色
@PreAuthorize("hasRole('ROLE_sale') AND hasRole('ROLE_manager')")
//@PreAuthorize("hasAnyAuthority('admins')")
public String update() {
return "hello update";
}

@PostAuthorize

@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限.

1
2
3
4
5
6
@GetMapping("update")
@PostAuthorize("hasAnyAuthority('admins')")
public String update() {
System.out.println("会执行这个输出语句....");
return "hello update";
}

@PostFilter

权限验证之后对数据进行过滤,留下用户名是 admin1 的数据,表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素。

1
2
3
4
5
6
7
8
@RequestMapping("getAll")
@PostFilter("filterObject.username == 'admin1'")
public List<UserInfo> getAllUser(){
ArrayList<UserInfo> list = new ArrayList<>();
list.add(new Users(11,"admin1","6666"));
list.add(new Users(21,"admin2","888"));
return list;
}

@PreFilter

进入控制器之前对数据进行过滤,只保留users.id是偶数的数据。

1
2
3
4
5
6
7
8
@RequestMapping("test")
@PreFilter(value = "filterObject.id%2==0")
public List<UserInfo> getTestPreFilter(@RequestBody List<Users> list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}

权限继承

1
2
3
4
5
6
7
@Bean
RoleHierarchy roleHierarchy() {
//admin拥有user的权限
RoleHierarchyImpl impl = new RoleHierarchyImpl();
impl.setHierarchy("ROLE_admin > ROLE_user");
return impl;
}

用户注销

1
2
3
4
5
6
@Override
protected void configure(HttpSecurity http) throws Exception {
//退出
http.logout().logoutUrl("/logout") //注销的URL
.logoutSuccessUrl("/test/hello").permitAll(); //注销后跳转的地址
}

自动登录

security

基于Cookie技术和安全框架机制实现自动登录

1
2
3
4
5
6
7
8
9
#rememberMe中将cookie写到数据库的表,代码中粘贴出来的
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注入数据源
@Autowired
private DataSource dataSource;

//配置操作数据库的对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
//设置数据源
jdbcTokenRepository.setDataSource(dataSource);
//自动创建表 第一次执行会创建,以后要执行就要删除掉!
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
//开启记住我功能
http.rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)//设置有效时长,单位秒
.userDetailsService(userDetailsService);//设置使用校验的用户Service
}
}
1
2
<!--html的表单中添加 checkbox , 此处的name值必须为 "remember-me" -->
<input type="checkbox" name="remember-me" title="自动登录"/><br/>

踢除其他登录

最简单的原理是保存用户ID和Session的映射关系,保证只有一个机器进行登录操作,新的机器登录时会踢出旧的登录Session。

1
2
3
4
5
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置一个用户最多对应一个Session会话
http.sessionManagement().maximumSessions(1);
}

禁止其他登录

1
2
3
4
5
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置一个用户对应一个Session会话,并且后来者无法进行登录
http.maximumSessions(1).maxSessionsPreventsLogin(true);
}

清理过期Session

1
2
3
4
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}

退出登录处理器

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addLogoutHandler(new LogoutHandler() {

@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
System.out.println("退出");
}
});
}

登录成功处理器

登录成功之后的处理操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.successHandler(new AuthenticationSuccessHandler() {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功");
// 根据权限不同,跳转到不同页面
request.getSession().getAttribute(name)
request.getRequestDispatcher("").forward(request, response);
}
});
}

登录失败处理器

1
2
3
4
5
6
7
8
9
10
11
//增加了Handler之后,则默认failureUrl配置不生效
.failureHandler(new AuthenticationFailureHandler() {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// TODO Auto-generated method stub
exception.printStackTrace();
request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
}
})
1
2
3
4
5
6
7
常见登录失败异常:
LockedException 账户被锁定
CredentialsExpiredException 密码过期
AccountExpiredException 账户过期
DisabledException 账户被禁用
BadCredentialsException 密码错误
UsernameNotFoundException 用户名错误

浏览器策略

同源

浏览器的同源策略是一种安全功能,同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

所谓同源是指:域名、协议、端口均相同。当出现域名、协议、端口中任意一个不同时,则不是同源的。

所以就产生了在www.abc.com网站中的Ajax请求到www.bcd.com网站的跨域问题。

security

1
2
3
4
5
6
1.浏览器发送HTTP请求到服务器
2.服务器发送给网关进行登录验证
3.网关登录验证,创建Session会话,返回会话标识SessionID/Token/令牌等标记信息
4.浏览器接受到SessionID/Token后,将其存储在Cookies中
5.下次发送HTTP请求时携带Cookies中的SessionID/Token到服务器中
6.定位同一个会话并访问

页面中常用的记住账号、自动登录等功能,最简单的实现就是在服务器中创建一个永不过期的会话,然后Cookies存储相关信息即可。

跨域

当访问一个服务器的网页时,它的Ajax请求到了另外一个非同源的连接时,则将触发跨域请求,Cookies一般情况下是无法跨域的。

1
2
3
4
jsonp解决跨域:
只能发起GET请求,通过jsonp发送的请求,会随带 cookie 一起发送。
Cors解决跨域:
在浏览器中指定Origin来源,如果在服务器接受范围,请求则成功

security

1
2
3
4
5
6
Cors解决跨域原理:
1.www.bcd.com服务器需要开启允许跨域 Access-Control-Allow-Origin: http://www.abc.com 标识允许这个域名对它进行跨域请求
1.www.abc.com页面中的Ajax跨域请求www.bcd.com
2.Ajax在请求的过程中需要携带自身请求的Cookies信息和Origin来源:www.abc.com到www.bcd.com中
3.www.bcd.com服务器校验请求的Origin来源是否处于可允许的列表中
4.www.bcd.com响应请求

XSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,当用户打开网页时,服务器自动加载并执行攻击者恶意制造的网页程序。
嵌入的脚本一般通过JS实现,如<script src = "www.baidu.com"/>,XSS嵌入的JS脚本攻击的只能发送get请求。

APP基本没有XSS跨站点攻击。

WEB解决办法:
1.防止非法JS嵌入到网页中
2.URL ENCODE特殊符号(%、;、&等等)
3.定期扫描静态资源是否匹配关键字(script、src)
4.敏感资源人机交互(图形验证码、手机验证码)
5.所有接口改成Post请求

APP的Token丢了解决办法:
1.代码混淆(编译后的文件加密,无法反编译)
2.Token绑定IP地址,切换IP地址则Token失效

CSRF

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。

这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

1
2
3
4
5
CSRF攻击原理:
1.用户登录受信任网站A,并且在本地生成Cookie
2.用户访问受信任网站A,浏览器自动携带Cookie,不需要再做任何验证
3.用户在未登出信任网站A时,访问了危险网站B
4.危险网站B通过浏览器同源,发送了一个请求到信任网站A中,以你的名义发送恶意请求

可以通过增加人为验证机制,或者每个请求动态生成Hash值解决。

Hash值放在Cookies中容易被CSRF利用进行二次请求,因为他能从Cookies中获取到Hash值,而把Hash值放在每次请求的页面中,即使用户没有退出受信任网站A,危险网站B也无法从信任网站A的页面中中获取Hash值。


从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

需要在页面表单中嵌入_csrf.token的值,每次后台都会下发一个新的csrfToken 到浏览器,并将生成 csrfToken 保存到 HttpSession 或者 Cookie 中。

1
2
3
4
5
<!-- html 写法-->
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />

<!-- thymeleaf 写法-->
<input type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}" name="_csrf"/>
1
2
3
4
5
6
7
8
//底层默认创建的Csrf配置类
public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<CsrfConfigurer<H>, H> {
//默认使用HttpSessionCsrfTokenRepository,也就是Session存储下发token的策略方式解决csrf攻击
//其他实现类:CookieCsrfTokenRepository
private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(
new HttpSessionCsrfTokenRepository());
}
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
//底层通过CsfrFilter过滤器对token做验证操作
public final class CsrfFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
//装载请求域中的token
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
//没token就创建token
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
//设置token到请求域中
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);

//验证token是否匹配
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}

//判断请求域的token和实际缓存的token是否一致
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));
}
return;
}

filterChain.doFilter(request, response);
}
}

微服务

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

单点登录

跨域是不同的系统内部互相通信时的浏览器不同源问题,而单点登录是指一个浏览器在系统A登录后,当浏览器访问系统B时,不需要再次登录。

security

1
2
3
4
5
6
7
单点登录过程:
1.浏览器向www.abc.com发起登录HTTP请求
2.不通的域名系统最终汇集到同一个网关上做鉴权操作
3.通过网关上的OAuth或者第三方的CAS鉴权,产生Session会话
4.持久化Session会话到Redis中,多个域名共享
5.返回SessionID/Token到浏览器中,缓存到Cookies中
6.请求www.bcd.com时,发送cookies中的SessonID/Token进行免登录。

security

自定义加密策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

//进行MD5加密
@Override
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}
//进行密码比对
@Override
public boolean matches(CharSequence charSequence, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
}

创建Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class TokenManager {
//token有效时长
private long tokenEcpiration = 24*60*60*1000;
//编码秘钥
private String tokenSignKey = "123456";
//1 使用jwt根据用户名生成token
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
return token;
}
//2 根据token字符串得到用户信息
public String getUserInfoFromToken(String token) {
String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return userinfo;
}
//3 删除token
public void removeToken(String token) { }
}

登出处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;

public TokenLogoutHandler(TokenManager tokenManager,RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//1 从header里面获取token
//2 token不为空,移除token,从redis删除token
String token = request.getHeader("token");
if(token != null) {
//移除
tokenManager.removeToken(token);
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
redisTemplate.delete(username);
}
//将请求结果写到response中返回
ResponseUtil.out(response, R.ok());
}
}

未授权处理器

1
2
3
4
5
6
7
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//返回error
ResponseUtil.out(httpServletResponse, R.error());
}
}

认证过滤器

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
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;

public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
//开放POST类型限制
this.setPostOnly(false);
//设置登录接口的url
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}

//1 获取表单提交用户名和密码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//获取表单提交数据
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
//将用户名和密码封装成对象交给Security管理
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),
new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}

//2 认证成功调用的方法
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//认证成功,得到认证成功之后用户信息
SecurityUser user = (SecurityUser)authResult.getPrincipal();
//根据用户名生成token
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
//把用户名称和用户权限列表放到redis
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
//返回token
ResponseUtil.out(response, R.ok().data("token",token));
}

//3 认证失败调用的方法
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
1
2
3
4
5
6
7
8
9
10
//"用户实体类"
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String nickName;
private String salt;
private String token;
}

授权过滤器

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
public class TokenAuthFilter extends BasicAuthenticationFilter {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;

public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获取当前认证成功用户权限信息
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
//判断如果有权限信息,放到权限上下文中
if(authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request,response);
}

private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//从header获取token
String token = request.getHeader("token");
if(token != null) {
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
//从redis获取对应权限列表
List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);
//用户具备的权限对象集合
Collection<GrantedAuthority> authority = new ArrayList<>();
for(String permissionValue : permissionValueList) {
//权限名称 需要使用SimpleGrantedAuthority包装成对象
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
authority.add(auth);
}
////将用户名和权限信息封装成对象交给Security管理
return new UsernamePasswordAuthenticationToken(username,token,authority);
}
return null;
}

}
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
//继承Security提供的用户类 UserDetails ,增加额外的属性,如 自定义的User类,权限集合
@Data
public class SecurityUser implements UserDetails {

//当前登录用户
private transient User currentUserInfo;

//当前权限
private List<String> permissionValueList;

public SecurityUser() {
}

public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}

//获取权限集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}

return authorities;
}

@Override
public String getPassword() {
return currentUserInfo.getPassword();
}

@Override
public String getUsername() {
return currentUserInfo.getUsername();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

核心配置类

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
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private DefaultPasswordEncoder defaultPasswordEncoder;
//自定义用户数据查询ervice
private UserDetailsService userDetailsService;

@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

/**
* 配置设置
* @param http
* @throws Exception
*/
//设置退出的地址和token,redis操作地址
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问
.and().csrf().disable()//关闭csrf
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")//退出路径
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
.addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
}

//调用userDetailsService和密码处理
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}

//不进行认证的路径,可以直接访问,如静态请求
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/img/**","/js/**");
}
}

自定义实现类

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
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

//Mybatis的用户表的Mapper
@Autowired
private UserService userService;

//Mybatis的权限表的Mapper
@Autowired
private PermissionService permissionService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询数据库数据 DbUser类是对应数据库用户表的实体类
DbUser user = userService.selectByUsername(username);
//判断
if(user == null) {
throw new UsernameNotFoundException("用户不存在");
}
//User类是对应SecurityUser implements UserDetails中的属性类User
User curUser = new User();
//通过工具类拷贝类的属性到另一个实体类中
BeanUtils.copyProperties(user,curUser);

//根据用户查询数据库的用户权限列表
List<String> permissionValueList = permissionService.selectPermissionValueByUserId(user.getId());

//构建SecurityUser(UserDetails)对象
SecurityUser securityUser = new SecurityUser();
securityUser.setCurrentUserInfo(curUser);
securityUser.setPermissionValueList(permissionValueList);
return securityUser;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用户认证流程:

#1.进入到过滤器验证登录
TokenLoginFilter.attemptAuthentication
#2.先通过查询数据库获取用户对象
UserDetailsService.loadUserByUsername
#3.验证用户通过
TokenLoginFilter.successfulAuthentication
#4.生成用户Token
tokenManager.createToken()
#5.将用户信息存储到Redis中
redisTemplate.opsForValue().set()
#6.response写入token
response.write(token)

认证成功后进入授权流程:
#7.获取当前用户的token,去redis中拉取权限信息,添加到Security对象权限
TokenAuthFilter.doFilterInternal()

过滤器介绍

SpringSecurity 的过滤器介绍

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的 15 个过滤器进行说明:

(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder 中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。

(3) HeaderWriterFilter:用于将头信息加入响应中。

(4) CsrfFilter:用于处理跨站请求伪造。

(5)LogoutFilter:用于处理退出登录。

(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改。

(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

(8)BasicAuthenticationFilter:检测和处理 http basic 认证。

(9)RequestCacheAwareFilter:用来处理请求的缓存。

(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。

(11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在Authentication 对象,如果不存在为其提供一个匿名 Authentication。

(12)SessionManagementFilter:管理 session 的过滤器

(13)ExceptionTranslationFilter:处理 AccessDeniedException 和AuthenticationException 异常。

(14)FilterSecurityInterceptor:可以看做过滤器链的出口。

(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

基本流程

Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:

security

绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在 configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:

UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。

ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由 ExceptionTranslationFilter 过滤器进行捕获和处理。

认证流程

security

当前端提交的是一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认证。该过滤器的 doFilter() 方法实现在其抽象父类。

security

分析UsernamePasswordAuthenticationFilter 认证过滤器,一般都是查看doFilter方法,该方法是在父类中实现的。

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
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware{
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

//1.判断提交方式是否Post,若不是则放行
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}

if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}

Authentication authResult;

try {
//2.调用子类的方法进行身份认证,认证成功之后,把认证信息封装到对象里面
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}

//3.session策略处理,如果配置了用户Session最大并发数,就是在此处进行判断处理
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);

//4.认证失败抛出异常,执行认证失败的方法
unsuccessfulAuthentication(request, response, failed);

return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);

return;
}

// 认证成功的处理,默认为false,所以认证成功后不进入下一个过滤器
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}

//5.认证成功,调用认证成功的方法
successfulAuthentication(request, response, chain, authResult);
}
}

然后再来详细分析子类的attemptAuthentication认证方法。

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
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {

//1.验证Post请求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

//2.获取表单提交的数据
String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

//3.使用获取的数据构造成对象,标记成未认证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

//4.把请求一些属性信息设置到对象里
setDetails(request, authRequest);

//5.调用方法进行身份认证(调用UserDetailsService)
return this.getAuthenticationManager().authenticate(authRequest);
}

security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();

//用户密码
Object getCredentials();
//请求携带的一些属性信息(例如:remoteAddress,sessionId)
Object getDetails();
//未认证时为前端请求传入的用户名,认证成功后为封装认证用户信息的UserDetails对象
Object getPrincipal();
//是否被认证
boolean isAuthenticated();
//设置是否被认证
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

以下类实现了Authentication接口,并通过参数的不同进行区分认证状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

private final Object principal;
private Object credentials;

//用于封装前端请求传入的未认证的用户信息
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
//设置认证状态
setAuthenticated(false);
}

//用于封装认证成功后的用户信息
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
}

上述过程中,UsernamePasswordAuthenticationFilter 过滤器的attemptAuthentication() 方法最后过程将未认证的 Authentication 对象传入ProviderManager 类的 authenticate() 方法进行身份认证。

security

ProviderManager 是 AuthenticationManager 接口的实现类,该接口是认证相关的核心接口,也是认证的入口。

在实际开发中,我们可能有多种不同的认证方式,例如:用户名+ 密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是AuthenticationManager。

在该接口的常用实现类 ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式(Delegate)的应用。

每种认证方式对应着一个 AuthenticationProvider,AuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托对应的 AuthenticationProvider 进行用户认证。

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
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
//传入未认证的Authentication对象
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//1.获取传入的Authentication类型,即 UsernamePasswordAuthenticationToken
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();

//2.迭代认证方式列表
for (AuthenticationProvider provider : getProviders()) {
//3.判断当前AuthenticationProvider是否适用UsernamePasswordAuthenticationToken类型
if (!provider.supports(toTest)) {
continue;
}

try {
//4.如果认证成功,会返回一个标记已认证的Authentication对象
result = provider.authenticate(authentication);

if (result != null) {
//5.认证成功后,将传入的Authentication对象中的details信息拷贝到已认证的Authentication对象中
copyDetails(authentication, result);
break;
}
} catch (AuthenticationException e) {
}
}

if (result == null && parent != null) {
// 6.认证失败,适用父类型 AuthenticationManager进行验证
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
}

if (result != null) {
//7.认证成功之后去除result的敏感信息,要求实现CredentialsContainer接口
//如去除用户密码缓存,默认的UsernamePasswordAuthenticationToken有实现
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {

((CredentialsContainer) result).eraseCredentials();
}

//8.发布认证成功的事件
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}

//9.认证失败抛出异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}

if (parentException == null) {
prepareException(lastException, authentication);
}

throw lastException;
}
}

上述过程就是认证流程的最核心部分,接下来重新回到UsernamePasswordAuthenticationFilter 过滤器的 doFilter() 方法,查看认证成功/失败的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//认证成功后的处理
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {

//1.将认证成功的用户信息对象封装到SecurityContext中,SecurityContextHolder是对ThreadLocal的封装
SecurityContextHolder.getContext().setAuthentication(authResult);
//2.rememberMe的处理
rememberMeServices.loginSuccess(request, response, authResult);

//3.发布认证成功的事件
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//4.调用认证成功处理器
successHandler.onAuthenticationSuccess(request, response, authResult);
}
1
2
3
4
5
6
7
8
9
10
11
//认证失败后的处理
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
//1.清除该线程在SecurityContextHolder中对应的SecurityContext对象
SecurityContextHolder.clearContext();
//2.rememberMe处理
rememberMeServices.loginFail(request, response);
//3.调用认证失败处理器
failureHandler.onAuthenticationFailure(request, response, failed);
}

security

权限访问流程

上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对ExceptionTranslationFilter 过滤器和 FilterSecurityInterceptor 过滤器进行介绍。

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
public class ExceptionTranslationFilter extends GenericFilterBean {

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

try {
//1.直接放行请求
chain.doFilter(request, response);

logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
//2.捕获出现的异常进行处理
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
//3.访问要认证的资源,但当前请求未认证所抛出的异常
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
//4.访问权限受限的资源所抛出的异常
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
}
}
}

FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器ExceptionTranslationFilter 进行捕获和处理。具体源码如下:

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
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}

public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {

if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}

//1.根据资源权限配置来判断当前请求是否有权限访问对应的资源。
//如果不能访问,则抛出相应的异常
InterceptorStatusToken token = super.beforeInvocation(fi);

try {
//2.访问相关资源,通过SpringMvc的核心组件DispatcherServlet进行访问
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}

super.afterInvocation(token, null);
}
}
}

需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件DispatcherServlet 运行之前。也就是说,请求通过 Spring Security 的所有过滤器,不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。

认证共享

在前面讲解认证成功的处理方法 successfulAuthentication() 时,有以下代码:

1
SecurityContextHolder.getContext().setAuthentication(authResult);
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
public class SecurityContextHolder {

//清空当前线程对应的ThreadLocal<SecurityContext>存储
public static void clearContext() {
strategy.clearContext();
}

//如果当前线程对应的ThreadLocal<SecurityContext>没有任何对象存储
//strategy.getContext会创建并返回一个空的SecurityContext对象
//并且该空的SecurityContext对象会存入ThreadLocal<SecurityContext>
public static SecurityContext getContext() {
return strategy.getContext();
}

private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
//默认使用MODE_THREADLOCAL模式
strategyName = MODE_THREADLOCAL;
}

//省略代码...
}

//设置当前线程对应的ThreadLocal<SecurityContext>存储
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}

前面提到过,在 UsernamePasswordAuthenticationFilter 过滤器认证成功之后,会在认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进SecurityContext,并存入 SecurityContextHolder。

之后,响应会通过 SecurityContextPersistenceFilter 过滤器,该过滤器的位置在所有过滤器的最前面,请求到来先进它,响应返回最后一个通过它,所以在该过滤器中处理已认证的用户信息对象 Authentication 与 Session 绑定。

认证成功的响应通过 SecurityContextPersistenceFilter 过滤器时,会从SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的SecurityContext,放进 Session 中。

当请求再次到来时,请求首先经过该过滤器,该过滤器会判断当前请求的 Session 是否存有 SecurityContext 对象,如果有则将该对象取出再次放入 SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访问不需要进行身份认证;

当响应再次返回时,该过滤器同样从 SecurityContextHolder 取出SecurityContext 对象,放入 Session 中。具体源码如下:

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
public class SecurityContextPersistenceFilter extends GenericFilterBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

//省略部分代码...

HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,response);

//1.请求到来时,检查当前Session中是否存有SecurityContext对象
//如果有,从Session中取出该对象,如果没有,创建一个空的SecurityContext对象
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

try {
//2.将上述获得SecurityContext对象放入SecurityContextHolder中
SecurityContextHolder.setContext(contextBeforeChainExecution);
//3.进入下一个过滤器
chain.doFilter(holder.getRequest(), holder.getResponse());

}
finally {
//4.响应返回时,从SecurityContextHolder中取出SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
//5.移除SecurityContextHolder中的SecurityContext
SecurityContextHolder.clearContext();
//6.将取出的SecurityContext对象放进Session
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
}
}
}

图形验证码

1
2
3
4
5
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
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
@Autowired
Producer captchaProducer;

//添加获取验证码图片的controller

@GetMapping("/kaptcha")
public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpSession session = request.getSession();
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
//创建图片验证码
String capText = captchaProducer.createText();

//每次请求时将最新的图片验证码存放到session会话的作用域中
session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
BufferedImage bi = captchaProducer.createImage(capText);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(bi, "jpg", out);
try {
out.flush();
} finally {
out.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//生成图片验证码的配置类
@Configuration
public class Kaconfig {
@Bean
public DefaultKaptcha getDefaultKaptcha(){
DefaultKaptcha captchaProducer = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty("kaptcha.border", "yes");
properties.setProperty("kaptcha.border.color", "105,179,90");
properties.setProperty("kaptcha.textproducer.font.color", "blue");
properties.setProperty("kaptcha.image.width", "310");
properties.setProperty("kaptcha.image.height", "240");
properties.setProperty("kaptcha.textproducer.font.size", "30");
properties.setProperty("kaptcha.session.key", "code");
properties.setProperty("kaptcha.textproducer.char.length", "4");
// properties.setProperty("kaptcha.textproducer.char.string", "678");
properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.ShadowGimpy");
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
Config config = new Config(properties);
captchaProducer.setConfig(config);
return captchaProducer;
}
}
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
//编写校验图片验证码的Filter过滤器
public class CodeFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse resp = (HttpServletResponse)response;

String uri = req.getServletPath();

if(uri.equals("/login") && req.getMethod().equalsIgnoreCase("post")) {
//从Session会话作用域中获取图片验证码
String sessionCode = req.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY).toString();
//获取表单中提交的图片验证码
String formCode = req.getParameter("code").trim();

//比对验证码是否正确
if(StringUtils.isEmpty(formCode)) {
throw new RuntimeException("验证码不能为空");
}
if(sessionCode.equalsIgnoreCase(formCode)) {
System.out.println("验证通过");
}
System.out.println(req.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY));
throw new AuthenticationServiceException("xx");
}

//若通过则调用Filter链
chain.doFilter(request, response);
}
}

校验图片验证码,最好在最外层的Filter中进行,不要让请求流转到校验账号密码的Filter中

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableWebSecurity
public class SecuritySecureConfig extends WebSecurityConfigurerAdapter {

//基于Http的请求配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new CodeFilter, UsernamePasswordAuthenticationFilter.class);
}
}

最后更新: 2020年12月09日 00:05

原始链接: https://midkuro.gitee.io/2020/06/05/springboot-security/

× 请我吃糖~
打赏二维码