1.前言

对自己长达10天左右的SpringSecurity框架网课的学习进行总结,包括SpringSecurity的用法、原理等。有不对的地方、不足的地方请大家及时指正。

2.初识SpringSecurity

2.1环境搭建

1.新建一个springboot工程
2.添加依赖,如下

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <!--security-->
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-security</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.projectlombok</groupId>
  12. <artifactId>lombok</artifactId>
  13. <optional>true</optional>
  14. </dependency>

3.编写一个Controller,写一个简单的接口

  1. @RestController
  2. public class DefaultDemoController {
  3. @GetMapping("query")
  4. public String query(){
  5. return "stu list";
  6. }
  7. }

4.项目目录查看
image.png

2.2启动服务并使用

1.启动程序
2.观察控制台的打印输出
image.png
3.通过浏览器访问自行编写的接口(query接口)
①浏览器输入(localhost:8080/query)
image.png
②回车后,会默认跳转到一个(localhost:8080/login)的页面
image.png ③输入用户 : user 密码 : 第2点 中控制台打印的 密码
image.png
④登录后,会返回接口(query)的返回值
image.png

2.3过程解释

1.没有加入springsecurity之前,我们访问接口是下图所示的模样
image.png
2.在加入springsecurity之后是下图的模样
image.png
3.因此,SpringSecurity在我们进行接口的访问前,进行了对访问者的身份进行认证,站在代码层面来讲,就是在我们访问Controller层代码之前,进入了SpringSecurity为我们提供的一些身份认证的一些代码里。这些代码便是SpringSecurity提供的一个个过滤器。

3.SpringSecurity的原理

3.1用户角度过程描述

1.用户访问了query接口
2.浏览器自动跳转到了(localhost:8080/login)登录页面==>(SpringSecurity做的)
3.用户输入用户名 user,密码(控制台打印的)
4.校验了用户名和密码,并跳转回(query接口)==>(SpringSecurity做的)
5.用户再次访问刷新query接口
6.能够返回query接口信息(SpringSecurity也有参与)

3.2代码角度过程描述

1.用户访问query接口,请求通过浏览器发往服务器(用户的操作)
2.SpringSecurity先拦截了这次请求,让这次请求走自己的一个过滤链(15个过滤器)。首次访问query,请求进入后服务器后,SpringSecurity没有此用户的登录信息,SpringSecurity会将其重定向到login页面,途中重要的过滤器如下:
image.png
3.用户输入用户名和密码
4.SpringSecurity会校验用户名和密码的逻辑,主要使用UserNamePasswordFilter进行用户信息的校验工作,并重定向到query接口。重定向后,请求再次进入SpringSecurity,由于SecuritySecurity中已经存在用户的认证信息,所以可以进行query的访问
image.png
5.用户再次刷新query接口
6.SpringSecurity中存在用户认证信息,所以可以直接访问。

3.3SpringSecurity形象描述

image.png

3.4SpringSecurity如何知道这次请求是哪个用户的?

1.在没有引进框架时,我们想做一个简单的登录时,如何判断这次请求是谁??
①解决方式:将用户信息放入 session中即可。
②原因:每次会话都会有一个sessionid被赋予,每个sessionid对应一个服务器上的唯一session。
2.SpringSecurity是怎么做的呢???
①默认情况下SpringSecurity也是这么做,将用户认证信息封装成一个SecurityContext对象存入session中。
②它是如何取得session中的用户信息的:
每次请求过来后,服务器会为这次请求开辟一个线程。SpringSecurity会根据sessionid,到对应session中取出SecurityContext对象(里面封装有用户信息),通过SecurityHolder工具类将SecurityContext放入这次请求的线程中,这样不同的请求使用不同的线程,对应线程中的SecurityContext也是自己的。
③当然Session中的SecurityContext对象,是在用户登录后放入Session中。

3.5认识加深

image.png

4.如何自定义自己的登录逻辑

在项目中,数据库中一定有用户的登录信息表,用户的信息一定是从数据库中查询出来的。如何使用自己的用户信息呢??

4.1演示项目编写

1.新建SpringBoot工程
2.引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-web</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.projectlombok</groupId>
  11. <artifactId>lombok</artifactId>
  12. </dependency>

3.编写一个与数据表对应的实体类

  1. @Data
  2. @AllArgsConstructor
  3. public class SysUser {
  4. private Long userId;
  5. private String userName;
  6. private String userPassword;
  7. private List<String> role;
  8. }

4.编写一个UserDetail类

  1. @Data
  2. public class MyUserDetail implements UserDetails {
  3. private SysUser sysUser;
  4. @Override
  5. public Collection<? extends GrantedAuthority> getAuthorities() {
  6. String[] roles = new String[sysUser.getRole().size()];
  7. sysUser.getRole().toArray(roles);
  8. List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(roles);
  9. return authorityList;
  10. }
  11. @Override
  12. public String getPassword() {
  13. return sysUser.getUserPassword();
  14. }
  15. @Override
  16. public String getUsername() {
  17. return sysUser.getUserName();
  18. }
  19. @Override
  20. public boolean isAccountNonExpired() {
  21. return true;
  22. }
  23. @Override
  24. public boolean isAccountNonLocked() {
  25. return true;
  26. }
  27. @Override
  28. public boolean isCredentialsNonExpired() {
  29. return true;
  30. }
  31. @Override
  32. public boolean isEnabled() {
  33. return true;
  34. }
  35. }

5.编写一个自定义的认证逻辑类

  1. @Component
  2. public class MyUserDetailService implements UserDetailsService {
  3. @Autowired
  4. private SysUserMapper sysUserMapper;
  5. @Override
  6. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  7. SysUser sysUser=sysUserMapper.selectByUserName(username);
  8. if(sysUser==null){
  9. throw UsernameNotFoundException();
  10. }
  11. MyUserDetail myUserDetail = new MyUserDetail();
  12. myUserDetail.setSysUser(sysUser);
  13. return myUserDetail;
  14. }
  15. }

6.编写SpringSecurity的配置信息

  1. @EnableWebSecurity
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Autowired
  4. private MyUserDetailService myUserDetailService;
  5. @Bean
  6. public PasswordEncoder passwordEncoder(){
  7. return new BCryptPasswordEncoder();
  8. }
  9. @Override
  10. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  11. auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
  12. }
  13. }

7.编写几个业务接口

  1. @RestController
  2. public class LoginServiceController {
  3. @GetMapping("query")
  4. public String query(){
  5. return "hello query";
  6. }
  7. @GetMapping("findById")
  8. public String findById(){
  9. return "this stu";
  10. }
  11. @GetMapping("findByUser")
  12. public String findByUser(){
  13. return "findByUser";
  14. }
  15. }

4.3认证流程(登录流程)

登录的逻辑执行是在UserNamePasswordFilter中完成的,具体的流程图如下:

SpringSecurity学习总结 - 图13

4.3UserDetail的要点

userDetail是个接口,只有返回这个用户信息的实体类,SpringSecurity才会帮我们做密码校验。其中覆盖的方法的含义

  1. @Data
  2. public class MyUserDetail implements UserDetails {
  3. private SysUser sysUser;
  4. //SpringSecurity会调用这个方法,去获取用户的角色信息
  5. @Override
  6. public Collection<? extends GrantedAuthority> getAuthorities() {
  7. String[] roles = new String[sysUser.getRole().size()];
  8. sysUser.getRole().toArray(roles);
  9. List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(roles);
  10. return authorityList;
  11. }
  12. //SpringSecurity会调用这个方法去获取用户密码,从而和用户输入密码做对比
  13. @Override
  14. public String getPassword() {
  15. return sysUser.getUserPassword();
  16. }
  17. //SpringSecurity会调用这个方法去获取用户名
  18. @Override
  19. public String getUsername() {
  20. return sysUser.getUserName();
  21. }
  22. //下面的返回值 有一个为false,此账号就不可用。
  23. //返回账号没过期
  24. @Override
  25. public boolean isAccountNonExpired() {
  26. return true;
  27. }
  28. //返回账号没被锁定
  29. @Override
  30. public boolean isAccountNonLocked() {
  31. return true;
  32. }
  33. //返回账号密码没有失效
  34. @Override
  35. public boolean isCredentialsNonExpired() {
  36. return true;
  37. }
  38. //返回账号是可用的
  39. @Override
  40. public boolean isEnabled() {
  41. return true;
  42. }
  43. }

4.4UserDetailService要点

SpringSecurity在做校验时,会调用自定义的UserDetailService,只要实现一个查询用户信息的方法即可,返回值必须是实现了UserDetail的类。

  1. @Component
  2. public class MyUserDetailService implements UserDetailsService {
  3. @Autowired
  4. private SysUserMapper sysUserMapper;
  5. @Override
  6. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  7. SysUser sysUser=sysUserMapper.selectByUserName(username);
  8. if(sysUser==null){
  9. throw UsernameNotFoundException();
  10. }
  11. MyUserDetail myUserDetail = new MyUserDetail();
  12. myUserDetail.setSysUser(sysUser);
  13. return myUserDetail;
  14. }
  15. }

5.SpringSecurity的记住我功能

记住我功能的实现方法:给浏览器一个固定过期时间的标记(cookie),服务器这边存储此cookie对应的用户信息。当浏览器携带cookie请求时,服务器根据cookie查找到对应的用户信息,放入本次访问的session中即可。

5.1实例代码

1.导入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-security</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>mysql</groupId>
  11. <artifactId>mysql-connector-java</artifactId>
  12. </dependency>
  13. <!--使用到mysql数据库进行 记住我的用户信息存储-->
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-jdbc</artifactId>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.projectlombok</groupId>
  20. <artifactId>lombok</artifactId>
  21. </dependency>

2.配置文件

  1. spring:
  2. datasource:
  3. driver-class-name: com.mysql.cj.jdbc.Driver
  4. url: jdbc:mysql://localhost:3306/gx?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
  5. username: root
  6. password: 888888
  7. type: com.zaxxer.hikari.HikariDataSource

3.静态页面(mylogin.html)

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录页面</title>
  6. </head>
  7. <body>
  8. <form action="/login" method="post">
  9. 用户名:<input name="username" type="text"><br>
  10. 密 码:<input name="password" type="password"><br>
  11. 记住我:<input type="radio" name="remember-me" value="true"><br>
  12. <input type="submit" value="登录">
  13. </form>
  14. </body>
  15. </html>

4.Security配置文件

  1. @EnableWebSecurity
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Autowired
  4. private HikariDataSource dataSource;
  5. @Bean
  6. public PasswordEncoder passwordEncoder(){
  7. return new BCryptPasswordEncoder();
  8. }
  9. /**
  10. *
  11. * @return
  12. */
  13. @Bean
  14. public PersistentTokenRepository persistentTokenRepository(){
  15. JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
  16. tokenRepository.setDataSource(dataSource);
  17. //若已经有相应的表格就不需要建立
  18. // tokenRepository.setCreateTableOnStartup(true);
  19. return tokenRepository;
  20. }
  21. @Override
  22. protected void configure(HttpSecurity http) throws Exception {
  23. http.formLogin()
  24. .loginPage("/mylogin.html")
  25. .loginProcessingUrl("/login")
  26. .and()
  27. .authorizeRequests().antMatchers("/mylogin.html").anonymous().anyRequest().authenticated()
  28. .and()
  29. .rememberMe().
  30. tokenRepository(persistentTokenRepository())
  31. .tokenValiditySeconds(30)
  32. .and().csrf().disable();
  33. }
  34. /**
  35. * 这里将用户放置到内存中
  36. * @param auth
  37. * @throws Exception
  38. */
  39. @Override
  40. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  41. auth.inMemoryAuthentication().withUser("gaoxi").password(passwordEncoder().encode("123456")).roles("admin");
  42. }
  43. }

5.Controller层代码

  1. @RestController
  2. public class RememberMeController {
  3. @RequestMapping("hello")
  4. public String hello(){
  5. return "hello";
  6. }
  7. }

6.目录结构
image.png

5.2运行过程

1.浏览器输入localhost:8080/hello(自动跳转到mylogin.html)
image.png
2.进行登录,后查看application
image.png
3.我将jessionID删除(就是关闭会话,此时在没有记住我功能时会继续让我们登录,但是有了记住我后)
删除jessionID
image.png
正常访问(并产生一个新的jsessionid)
image.png

5.3记住我功能原理

1.在选择记住我方式登录时,在认证成功后,会在数据库中存入一条记住我的tokne信息。并且生成一个remember-me的cookie
2.当用户携带着remember-me的cookie,并且没有认证信息时,RememberMeAuthenticationFilter 会解析remember-me的cookie中的value值,通过value值查询数据库中存放的记住我token信息。
3.通过数据库中存放的 用户名字段,调用 UserDetailService的loginByUsername接口,查询用户信息,并封装成认证信息对象放入SecurityContext中,最后存入Session。