1. 初识SpringSecurity
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
注意:SpringSecurity一般搭配SpringBoot
why?
相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security。
推荐的组合:
SSM+Shiro
Spring Boot/Spring Cloud + Spring Security
2. 简单了解一下CSRF攻击
2.1 简介
CSRF(Cross Site Request Forgery, 跨站域请求伪造)是一种网络的攻击方式,它在 2007 年曾被列为互联网 20 大安全隐患之一。
2.2 攻击原理
网站是通过cookie来实现登录功能的。而cookie只要存在浏览器中,那么浏览器在访问这个cookie的服务器的时候,就会自动的携带cookie信息到服务器上去。那么这时候就存在一个漏洞了,如果你访问了一个别有用心或病毒网站,这个网站可以在网页源代码中插入js代码,使用js代码给其他服务器发送请求(比如ICBC的转账请求)。那么因为在发送请求的时候,浏览器会自动的把cookie发送给对应的服务器,这时候相应的服务器(比如ICBC网站),就不知道这个请求是伪造的,就被欺骗过去了。从而达到在用户不知情的情况下,给某个服务器发送了一个请求(比如转账)。
2.3 防御CSRF攻击
CSRF攻击的要点就是在向服务器发送请求的时候,相应的cookie会自动的发送给对应的服务器。造成服务器不知道这个请求是用户发起的还是伪造的。这时候,我们可以在用户每次访问有表单的页面的时候,在网页源代码中加一个随机的字符串叫做csrf_token,在cookie中也加入一个相同值的csrf_token字符串。以后给服务器发送请求的时候,必须在body中以及cookie中都携带csrf_token,服务器只有检测到cookie中的csrf_token和body中的csrf_token都相同,才认为这个请求是正常的,否则就是伪造的。那么黑客就没办法伪造请求了。
3. 简单配置一下
3.1、引入 Spring Security 模块
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
3.2 写一个简单的controller层
/*** @author 袁梓为* @Description* @date Created in 17:56 2022/7/15*/@RestControllerpublic class helloController {@RequestMapping("/hello")public String showHello(){return "hello SpringSecurity";}}
3.3 运行访问
浏览器输入localhost:8080/hello访问后发现,浏览器自动跳转到localhost:8080/login
这个页面长这样:
输入默认用户名:user
如图,密码在控制台会有提示:c389e9e3-563c-4a99-bb5c-e2cbe3ba179b
输入用户名和密码后,成功进入页面,可以看到返回给前端的字符串:hello SpringSecurity
如下图:
4. SpringSecurity基本原理
4.1 本质:过滤器链
从启动是可以获取到过滤器链:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilterorg.springframework.security.web.context.SecurityContextPersistenceFilterorg.springframework.security.web.header.HeaderWriterFilterorg.springframework.security.web.csrf.CsrfFilterorg.springframework.security.web.authentication.logout.LogoutFilterorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilterorg.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilterorg.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilterorg.springframework.security.web.savedrequest.RequestCacheAwareFilterorg.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilterorg.springframework.security.web.authentication.AnonymousAuthenticationFilterorg.springframework.security.web.session.SessionManagementFilterorg.springframework.security.web.access.ExceptionTranslationFilterorg.springframework.security.web.access.intercept.FilterSecurityInterceptor
4.2 粗略了解几个过滤器

如上图所示,Spring Security包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。
①UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。
用于处理基于表单方式的登录认证

②BasicAuthenticationFilter用于处理基于HTTP Basic方式的登录验证
③ExceptionTranslationFilter:异常过滤器,用来处理在认证授权过程中抛出的异常

ExceptionTranslateFilter捕获并处理,所以我们用ExceptionTranslateFilter过滤器对FilterSecurityInterceptor抛出的异常进行处理,比如需要身份认证时将请求重定向到相应的认证页面,当认证失败或者权限不足时返回相应的提示信息。
④FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。
在过滤器链的末尾是一个名为FilterSecurityInterceptor的拦截器,用于判断当前请求身份认证是否成功,是否有相应的权限,当身份认证失败或者权限不足的时候便会抛出相应的异常。

super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。
fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 表示真正的调用后台的服务。
5.SpringSecurity自定义用户认证
自定义认证的过程需要实现Spring Security提供的UserDetailService接口,该接口只有一个抽象方法loadUserByUsername,源码如下:
public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}
loadUserByUsername方法返回一个UserDetail对象,该对象也是一个接口,包含一些用于描述用户信息的方法,源码如下:
public interface UserDetails extends Serializable {//获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象;Collection<? extends GrantedAuthority> getAuthorities();//用于获取密码;String getPassword();//用于获取用户名;String getUsername();//用于判断账户是否未过期,未过期返回true反之返回false;boolean isAccountNonExpired();//判断账户是否未锁定boolean isAccountNonLocked();//用于判断用户凭证是否没过期,即密码是否未过期;boolean isCredentialsNonExpired();//用于判断用户是否可用boolean isEnabled();}
4.3 UserDetailsService接口
实际中我们可以自定义UserDetails接口的实现类,也可以直接使用Spring Security提供的UserDetails接口实现类org.springframework.security.core.userdetails.User。
具体操作:
4.3.1 创建一个MyUser对象
说了那么多,下面我们来开始实现UserDetailService接口的loadUserByUsername方法。
首先创建一个MyUser对象,用于存放模拟的用户数据(实际中一般从数据库获取,这里为了方便直接模拟):
public class MyUser implements Serializable {private static final long serialVersionUID = 3497935890426858541L;private String userName;private String password;private boolean accountNonExpired = true;private boolean accountNonLocked= true;private boolean credentialsNonExpired= true;private boolean enabled= true;// get,set略}
4.3.2 创建MyUserDetailService实现UserDetailService接口:
@Configurationpublic class UserDetailService implements UserDetailsService {@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 模拟一个用户,替代数据库获取逻辑MyUser user = new MyUser();user.setUserName(username);user.setPassword(this.passwordEncoder.encode("123456"));// 输出加密后的密码System.out.println(user.getPassword());return new User(username, user.getPassword(), user.isEnabled(),user.isAccountNonExpired(), user.isCredentialsNonExpired(),user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));}}
注意:
这里我们使用了org.springframework.security.core.userdetails.User类包含7个参数的构造器,其还包含一个三个参数的构造器User(String username, String password,Collection<? extends GrantedAuthority> authorities),由于权限参数不能为空,所以这里先使用**AuthorityUtils.commaSeparatedStringToAuthorityList**方法模拟一个admin的权限,该方法可以将逗号分隔的字符串转换为权限集合。
4.3.3 配置BrowserSecurityConfig
此外我们还注入了PasswordEncoder对象,该对象用于密码加密,注入前需要手动配置。我们在BrowserSecurityConfig中配置它:
@Configurationpublic class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}...}
注意:
PasswordEncoder是一个密码加密接口,而BCryptPasswordEncoder是Spring Security提供的一个实现方法,我们也可以自己实现PasswordEncoder。不过Spring Security实现的BCryptPasswordEncoder已经足够强大,它对相同的密码进行加密后可以生成不同的结果。
这时候重启项目,访问http://localhost:8080/login,便可以使用任意用户名以及123456作为密码登录系统。我们**多次进行登录操作**,可以看到控制台输出的加密后的密码如下:
可以看到,**BCryptPasswordEncoder**对相同的密码生成的结果每次都是不一样的。
4.3.4自定义登录页
默认的登录页面过于简陋,我们可以自己定义一个登录页面。为了方便起见,我们直接在src/main/resources/resources目录下定义一个login.html(不需要Controller跳转):
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>登录</title><link rel="stylesheet" href="css/login.css" type="text/css"></head><body><form class="login-page" action="/login" method="post"><div class="form"><h3>账户登录</h3><input type="text" placeholder="用户名" name="username" required="required" /><input type="password" placeholder="密码" name="password" required="required" /><button type="submit">登录</button></div></form></body></html>
4.3.5跳转到自定义登录页面
要怎么做才能让Spring Security跳转到我们自己定义的登录页面呢?很简单,只需要在BrowserSecurityConfig的configure中添加一些配置:
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() // 表单登录// http.httpBasic() // HTTP Basic//指定了跳转到登录页面的请求URL.loginPage("/login.html")//对应登录页面form表单的action="/login".loginProcessingUrl("/login").and().authorizeRequests() // 授权配置//表示跳转到登录页面的请求不被拦截,否则会进入无限循环。.antMatchers("/login.html").permitAll().anyRequest() // 所有请求.authenticated(); // 都需要认证}
