1.前言
对自己长达10天左右的SpringSecurity框架网课的学习进行总结,包括SpringSecurity的用法、原理等。有不对的地方、不足的地方请大家及时指正。
2.初识SpringSecurity
2.1环境搭建
1.新建一个springboot工程
2.添加依赖,如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
3.编写一个Controller,写一个简单的接口
@RestController
public class DefaultDemoController {
@GetMapping("query")
public String query(){
return "stu list";
}
}
2.2启动服务并使用
1.启动程序
2.观察控制台的打印输出
3.通过浏览器访问自行编写的接口(query接口)
①浏览器输入(localhost:8080/query)
②回车后,会默认跳转到一个(localhost:8080/login)的页面
③输入用户 : user 密码 : 第2点 中控制台打印的 密码
④登录后,会返回接口(query)的返回值
2.3过程解释
1.没有加入springsecurity之前,我们访问接口是下图所示的模样
2.在加入springsecurity之后是下图的模样
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页面,途中重要的过滤器如下:
3.用户输入用户名和密码
4.SpringSecurity会校验用户名和密码的逻辑,主要使用UserNamePasswordFilter进行用户信息的校验工作,并重定向到query接口。重定向后,请求再次进入SpringSecurity,由于SecuritySecurity中已经存在用户的认证信息,所以可以进行query的访问
5.用户再次刷新query接口
6.SpringSecurity中存在用户认证信息,所以可以直接访问。
3.3SpringSecurity形象描述
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认识加深
4.如何自定义自己的登录逻辑
在项目中,数据库中一定有用户的登录信息表,用户的信息一定是从数据库中查询出来的。如何使用自己的用户信息呢??
4.1演示项目编写
1.新建SpringBoot工程
2.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3.编写一个与数据表对应的实体类
@Data
@AllArgsConstructor
public class SysUser {
private Long userId;
private String userName;
private String userPassword;
private List<String> role;
}
4.编写一个UserDetail类
@Data
public class MyUserDetail implements UserDetails {
private SysUser sysUser;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
String[] roles = new String[sysUser.getRole().size()];
sysUser.getRole().toArray(roles);
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(roles);
return authorityList;
}
@Override
public String getPassword() {
return sysUser.getUserPassword();
}
@Override
public String getUsername() {
return sysUser.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;
}
}
5.编写一个自定义的认证逻辑类
@Component
public class MyUserDetailService implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser=sysUserMapper.selectByUserName(username);
if(sysUser==null){
throw UsernameNotFoundException();
}
MyUserDetail myUserDetail = new MyUserDetail();
myUserDetail.setSysUser(sysUser);
return myUserDetail;
}
}
6.编写SpringSecurity的配置信息
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailService myUserDetailService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
}
}
7.编写几个业务接口
@RestController
public class LoginServiceController {
@GetMapping("query")
public String query(){
return "hello query";
}
@GetMapping("findById")
public String findById(){
return "this stu";
}
@GetMapping("findByUser")
public String findByUser(){
return "findByUser";
}
}
4.3认证流程(登录流程)
登录的逻辑执行是在UserNamePasswordFilter
中完成的,具体的流程图如下:
4.3UserDetail的要点
userDetail是个接口,只有返回这个用户信息的实体类,SpringSecurity才会帮我们做密码校验。其中覆盖的方法的含义
@Data
public class MyUserDetail implements UserDetails {
private SysUser sysUser;
//SpringSecurity会调用这个方法,去获取用户的角色信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
String[] roles = new String[sysUser.getRole().size()];
sysUser.getRole().toArray(roles);
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(roles);
return authorityList;
}
//SpringSecurity会调用这个方法去获取用户密码,从而和用户输入密码做对比
@Override
public String getPassword() {
return sysUser.getUserPassword();
}
//SpringSecurity会调用这个方法去获取用户名
@Override
public String getUsername() {
return sysUser.getUserName();
}
//下面的返回值 有一个为false,此账号就不可用。
//返回账号没过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//返回账号没被锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//返回账号密码没有失效
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//返回账号是可用的
@Override
public boolean isEnabled() {
return true;
}
}
4.4UserDetailService要点
SpringSecurity在做校验时,会调用自定义的UserDetailService,只要实现一个查询用户信息的方法即可,返回值必须是实现了UserDetail的类。
@Component
public class MyUserDetailService implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser=sysUserMapper.selectByUserName(username);
if(sysUser==null){
throw UsernameNotFoundException();
}
MyUserDetail myUserDetail = new MyUserDetail();
myUserDetail.setSysUser(sysUser);
return myUserDetail;
}
}
5.SpringSecurity的记住我功能
记住我功能的实现方法:给浏览器一个固定过期时间的标记(cookie),服务器这边存储此cookie对应的用户信息。当浏览器携带cookie请求时,服务器根据cookie查找到对应的用户信息,放入本次访问的session中即可。
5.1实例代码
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--使用到mysql数据库进行 记住我的用户信息存储-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2.配置文件
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/gx?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 888888
type: com.zaxxer.hikari.HikariDataSource
3.静态页面(mylogin.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input name="username" type="text"><br>
密 码:<input name="password" type="password"><br>
记住我:<input type="radio" name="remember-me" value="true"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
4.Security配置文件
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private HikariDataSource dataSource;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
*
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//若已经有相应的表格就不需要建立
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/login")
.and()
.authorizeRequests().antMatchers("/mylogin.html").anonymous().anyRequest().authenticated()
.and()
.rememberMe().
tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(30)
.and().csrf().disable();
}
/**
* 这里将用户放置到内存中
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("gaoxi").password(passwordEncoder().encode("123456")).roles("admin");
}
}
5.Controller层代码
@RestController
public class RememberMeController {
@RequestMapping("hello")
public String hello(){
return "hello";
}
}
5.2运行过程
1.浏览器输入localhost:8080/hello(自动跳转到mylogin.html)
2.进行登录,后查看application
3.我将jessionID删除(就是关闭会话,此时在没有记住我功能时会继续让我们登录,但是有了记住我后)
删除jessionID
正常访问(并产生一个新的jsessionid)
5.3记住我功能原理
1.在选择记住我方式登录时,在认证成功后,会在数据库中存入一条记住我的tokne信息。并且生成一个remember-me的cookie
2.当用户携带着remember-me的cookie,并且没有认证信息时,RememberMeAuthenticationFilter
会解析remember-me的cookie中的value值,通过value值查询数据库中存放的记住我token信息。
3.通过数据库中存放的 用户名字段,调用 UserDetailService的loginByUsername接口,查询用户信息,并封装成认证信息对象放入SecurityContext中,最后存入Session。