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_tokenbody中的csrf_token都相同,才认为这个请求是正常的,否则就是伪造的。那么黑客就没办法伪造请求了。

3. 简单配置一下

3.1、引入 Spring Security 模块

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

3.2 写一个简单的controller层

  1. /**
  2. * @author 袁梓为
  3. * @Description
  4. * @date Created in 17:56 2022/7/15
  5. */
  6. @RestController
  7. public class helloController {
  8. @RequestMapping("/hello")
  9. public String showHello(){
  10. return "hello SpringSecurity";
  11. }
  12. }

3.3 运行访问

浏览器输入localhost:8080/hello访问后发现,浏览器自动跳转到localhost:8080/login
这个页面长这样:
image.png

输入默认用户名:user

如图,密码在控制台会有提示:c389e9e3-563c-4a99-bb5c-e2cbe3ba179b
image.png

输入用户名和密码后,成功进入页面,可以看到返回给前端的字符串:hello SpringSecurity
如下图:
image.png

4. SpringSecurity基本原理

4.1 本质:过滤器链

从启动是可以获取到过滤器链:

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

4.2 粗略了解几个过滤器

image.png

如上图所示,Spring Security包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。

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

用于处理基于表单方式的登录认证

image.png

②BasicAuthenticationFilter用于处理基于HTTP Basic方式的登录验证

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

image.png

ExceptionTranslateFilter捕获并处理,所以我们用ExceptionTranslateFilter过滤器对FilterSecurityInterceptor抛出的异常进行处理,比如需要身份认证时将请求重定向到相应的认证页面,当认证失败或者权限不足时返回相应的提示信息。

④FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。

在过滤器链的末尾是一个名为FilterSecurityInterceptor的拦截器,用于判断当前请求身份认证是否成功,是否有相应的权限,当身份认证失败或者权限不足的时候便会抛出相应的异常。

image.png
super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。
fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 表示真正的调用后台的服务。

5.SpringSecurity自定义用户认证

自定义认证的过程需要实现Spring Security提供的UserDetailService接口,该接口只有一个抽象方法loadUserByUsername,源码如下:

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

loadUserByUsername方法返回一个UserDetail对象,该对象也是一个接口,包含一些用于描述用户信息的方法,源码如下:

  1. public interface UserDetails extends Serializable {
  2. //获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象;
  3. Collection<? extends GrantedAuthority> getAuthorities();
  4. //用于获取密码;
  5. String getPassword();
  6. //用于获取用户名;
  7. String getUsername();
  8. //用于判断账户是否未过期,未过期返回true反之返回false;
  9. boolean isAccountNonExpired();
  10. //判断账户是否未锁定
  11. boolean isAccountNonLocked();
  12. //用于判断用户凭证是否没过期,即密码是否未过期;
  13. boolean isCredentialsNonExpired();
  14. //用于判断用户是否可用
  15. boolean isEnabled();
  16. }

4.3 UserDetailsService接口

实际中我们可以自定义UserDetails接口的实现类,也可以直接使用Spring Security提供的UserDetails接口实现类org.springframework.security.core.userdetails.User

具体操作:

4.3.1 创建一个MyUser对象

说了那么多,下面我们来开始实现UserDetailService接口的loadUserByUsername方法。

首先创建一个MyUser对象,用于存放模拟的用户数据(实际中一般从数据库获取,这里为了方便直接模拟):

  1. public class MyUser implements Serializable {
  2. private static final long serialVersionUID = 3497935890426858541L;
  3. private String userName;
  4. private String password;
  5. private boolean accountNonExpired = true;
  6. private boolean accountNonLocked= true;
  7. private boolean credentialsNonExpired= true;
  8. private boolean enabled= true;
  9. // get,set略
  10. }

4.3.2 创建MyUserDetailService实现UserDetailService接口:
  1. @Configuration
  2. public class UserDetailService implements UserDetailsService {
  3. @Autowired
  4. private PasswordEncoder passwordEncoder;
  5. @Override
  6. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  7. // 模拟一个用户,替代数据库获取逻辑
  8. MyUser user = new MyUser();
  9. user.setUserName(username);
  10. user.setPassword(this.passwordEncoder.encode("123456"));
  11. // 输出加密后的密码
  12. System.out.println(user.getPassword());
  13. return new User(username, user.getPassword(), user.isEnabled(),
  14. user.isAccountNonExpired(), user.isCredentialsNonExpired(),
  15. user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
  16. }
  17. }

注意

这里我们使用了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中配置它:

  1. @Configuration
  2. public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Bean
  4. public PasswordEncoder passwordEncoder() {
  5. return new BCryptPasswordEncoder();
  6. }
  7. ...
  8. }

注意:

PasswordEncoder是一个密码加密接口,而BCryptPasswordEncoder是Spring Security提供的一个实现方法,我们也可以自己实现PasswordEncoder。不过Spring Security实现的BCryptPasswordEncoder已经足够强大,它对相同的密码进行加密后可以生成不同的结果。

这时候重启项目,访问http://localhost:8080/login,便可以使用任意用户名以及123456作为密码登录系统。我们**多次进行登录操作**,可以看到控制台输出的加密后的密码如下:

可以看到,**BCryptPasswordEncoder**对相同的密码生成的结果每次都是不一样的。

4.3.4自定义登录页

默认的登录页面过于简陋,我们可以自己定义一个登录页面。为了方便起见,我们直接在src/main/resources/resources目录下定义一个login.html(不需要Controller跳转):

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录</title>
  6. <link rel="stylesheet" href="css/login.css" type="text/css">
  7. </head>
  8. <body>
  9. <form class="login-page" action="/login" method="post">
  10. <div class="form">
  11. <h3>账户登录</h3>
  12. <input type="text" placeholder="用户名" name="username" required="required" />
  13. <input type="password" placeholder="密码" name="password" required="required" />
  14. <button type="submit">登录</button>
  15. </div>
  16. </form>
  17. </body>
  18. </html>

4.3.5跳转到自定义登录页面

要怎么做才能让Spring Security跳转到我们自己定义的登录页面呢?很简单,只需要在BrowserSecurityConfigconfigure中添加一些配置:

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. http.formLogin() // 表单登录
  4. // http.httpBasic() // HTTP Basic
  5. //指定了跳转到登录页面的请求URL
  6. .loginPage("/login.html")
  7. //对应登录页面form表单的action="/login"
  8. .loginProcessingUrl("/login")
  9. .and()
  10. .authorizeRequests() // 授权配置
  11. //表示跳转到登录页面的请求不被拦截,否则会进入无限循环。
  12. .antMatchers("/login.html").permitAll()
  13. .anyRequest() // 所有请求
  14. .authenticated(); // 都需要认证
  15. }

4.4PasswordEncoder接口

5. JWT(Json Web Token)