框架基础

SpringSecurity是一个功能强大且高度可定制的身份认证和访问控制框架。它是保护基于
Spring的应用程序的事实上的标准。(Shiro 框架)
SpringSecurity是一个致力于为Java应用程序提供身份认证授权的框架。像所有Spring
项目一样,SpringSecurity的真正强大之处在于它可以非常轻松地扩展来满足自定义需求;

Spring Security快速上手

spring security现在开发时不会采用spring 进行开发,而是采用spring boot进行开发;

依赖:

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

建立项目的时候要勾选Spring Security

创建好后会发现多出这三个文件,下面两个一个是win脚本,一个是Linux脚本,还有个上面的是配置文件,里面的链接下载一个maven什么的rar压缩文件,是用于打包的,这三个删掉就行,因为可以用maven的package打包,还有个help.md也可以删掉

image-20260304180753211

先随便创建一个controller

1
2
3
4
5
6
@RestController//这个注解,返回字符串或者json,@Controller注解跳转到页面
public class UserController{
@RequestMapping(value ="/hello")
public String hello() {
return "Hello,SpringSecurity."//返回字符串
}

启动项目,控制台会打印下面信息,这个密码是临时的,格式是uuid,这个密码仅限于开发者使用,不能用于产品

image-20260304181522193

然后访问localhost:8080//hello,然后发现页面进行了跳转到localhost:8080/login,这个也是重定向,输入用户名user密码uuid,然后就会跳转到字符串输出页面,打开网页控制台的cookie里发现有了session,image-20260304181837717

注意登录成功后默认是跳转回上一个页面

SpringSecurity基本原理分析

SpringSecurity采用16个Filter进行过滤拦截;(基于session),不同的版本filter数量可能有变化

入口:FilterChainProxy

image-20260304182750138

DefaultLoginPageGeneratingFilter生成登录的页面;
DefaultLogoutPageGeneratingFilter生成退出的页面;
登录跳转地址是:/login(这是SpringSecurity框架提供的,不是我们写的)
退出跳转地址是:/logout(这是SpringSecurity框架提供的,不是我们写的)
默认情况下,用户名是user,密码是临时生成的uuid;(来自SecurityProperties类),点进去可以看到代码

1
2
String name = "user";
String password = UUID.randomUUID().toString():

可以修改默认的用户名和密码,在配置文件application.properties中配置:

1
2
3
#自己指定登录的用户名和密码
spring.security.user.name=cat
spring.security.user.password=aaall1

如果是yml就是如下

1
2
3
4
5
spring:
security:
user:
name:cat
password: aaa111

SpringSecurity框架登录认证

具体步骤是先建项目,然后加依赖,配文件再写代码

配置文件yml里数据库链接词(type)如果不写的话默认是com.zaxxer:HikariCP这个

1
2
3
4
5
6
7
8
spring:
application:这个是模块名,可以不写
name:security-02-db-login
datasource:
username: root
password: 123
url: jdbc:mysql://localhost:3306/dlyk
driver-class-name: com.mysql.cj.jdbc.Driver

然后把数据库导入,然后新建个controller,这个是控制登录的

1
2
3
4
5
6
7
8
9
@RestController//返回字符串或json,这样我们测试更便捷
public class UserController {

//http://localhost:8080/--> 没登录 -->http://localhost:8080/login
//http://localhost:8080/--> 登录了 -->返回"WelcometoSpringSecurity."字符串
@RequestMapping(value ="/")
public String index(
return "Welcome to Spring Security.";
}

这里我们想用数据库里的数据登,这里统一密码是aaa111,但直接登录是一定失败的,因为没写代码实现,直接登录默认用户名user密码是uuid,这里的controller不用动,登录会提交给Spring Security,只需要写Service就行

1
2
//我们的处理登录的service接口,需要继承springsecurity框架的UserDetailsService接口
public interface UserService extends UserDetailsService{}

这个UserDetailsService点进去可以看到有个抽象方法,这个抽象方法被继承了就相当于也有了这个方法,需要去实现

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

当前端输完点击登录,这个请求就交给Spring Security,框架会调用下面这个方法,并且把前端的用户名传过来,然后使用逆向工程根据数据表生成接口,实现这步要在idea下载free mybatis tool这个插件,然后在idea里连接数据库并生成,具体的看https://www.bilibili.com/video/BV1RSwXeTE5w?spm_id_from=333.788.player.switch&vd_source=b0128143be22d1783a42f5fbf34d9d25&p=10
生成好了后需要把dao变成bean,有两种方法,一种是在具体的mapper里加上@Mapper注解,这样可以动态代理,如果mapper比较多的话就在Application那个类里加上@MapperScan(value = {‘com.bjpowernode.mapper’})这样可以扫描整个包

@Resource和@Autowired的区别:

  1. @Resource是javaee提供的标准,@Autowired是Spring提供的
  2. Resource先名称再字段名再类型注入,Autowired则顺序相反

注入后写查询语句,写完后还要抛出异常

返回的类型不能是单纯的tUser,得是框架里的,而UserDetails是个接口,点进去可以查看,光标点UserDetails这个接口名按ctrl+h就可以看到实现类,有个User,所以要用这个来构建,构建好后赋给userDetails(因为最后的build的返回值是userDetails,要用这个接口去接收)但实际上还是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
25
26
27
28
29
30
31
32
@Service
public class UserServiceImpl implements UserService{

//逆向工程、反向工程(根据数据库表,生成mapper接口、mapper.xml、实体类)
//和Autowired一样都是自动注入
@Resource
private TUserMapper tUserMapper;


/**
*该方法在springsecurity框架登录的时候被调用
* @param Username
Oreturn
* @throwsUsernameNotFoundException
*
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
//查询数据库,查询页面上传过来的这个用户名是否在数据库中存在,也就是根据该username查询用户对象
TUser tuser = tUserMapper.selectByLoginAct(username);
if (tUser == null){
throw new UsernameNotFoundException("登录账号不存在");//抛出异常
}

//构建一个SpringSecurity框架里面的User对象来返回
UserDetails userDetails = User.builder(
.username(tUser.getLoginAct())
.password(tUser.getLoginPwd())
.authorities(AuthorityUtils.NO_AUTHoRITIES)//权限是空的,用这个常量来写
.build();//返回值是userDetails,要用这个接口去接收,但实际上还是User类型
return userDetails;//把UserDetails(User)返回给框架之后,框架会采用密妈加密器进行密码的比较
}
1
2
3
public interface TUserMapper{
TUser selectByLoginAct(String loginAct);
}
1
2
3
4
5
6
<select id="selectByLoginAct" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_user
where login_act = #{loginAct,jdbcType=VARCHAR}
</select>

运行时如果报错Invalid bound statement (not found): com.bjpowernode.mapper.TUserMapper.selectByLoginAct这个的原因是没有帮我们编译,需要在pom里加个resource标签,或者手动使用maven的compile的编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>

配置密码加密器

现在还有个问题是现在只是查询用户,密码的比对怎么办呢,框架提醒我们还需要一个密码加密器,如果没有密码加密器会报以下错误

老版本报错:java.lang.llegalArgumentException:You have entered a password with no PasswordEncoder.
新版本报错:You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with ‘(noop}’.

1
2
3
4
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

新建一个配置类,这个@Beam的效果等同于注释的标签

1
2
3
4
5
6
7
8
9
10
11
@Configuration//配置spring的容器,类似spring.xml文件一样
public class SecurityConfig{
/**
*<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"> </bean>效果等同于这个
* @return
*/
@Bean//配置一个spring的bean,bean的id就是方法名,bean的class就是方法的返回类型
//现在版本可能直接是BCryptPasswordEncoder,找不到PasswordEncoder
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

SpringSecurity数据库登录流程分析

  1. 访问 http://localhost:8080/
  2. 被spring security 的 filter过滤器拦截(里面有 15个Filter);
  3. 由于没有登录过,所以springsecurity就跳转到登录页(登录页是框架生成的)
  4. 我们在登录页输入账号和密码去登录提交;(账号密码来自数据库)
  5. spring security里面的UsernamePasswordAuthenticationFilter接收账号和密码;
  6. 第 5步的这个 filter会调用loadUserByUsername(String username)方法去DB查询用户;
  7. 从数据库查询到用户后,把用户组装成UserDetail对象,然后返回给SpringSecurity框架;
  8. 第7步返回后,再回到框架的filter里面进行用户状态的判断,用户对象中默认有4个状态字段,如果这4个状态字段的值都是true,该用户才能登录,否则就是提示用户状态不正常,不能登录的(框架中实际上只判断3个状态值,那个密码是否过期没有做判断);
  9. 第7步返回后,再回到框架的filter里面进行密码的匹配,如果密码匹配上了,就登录成
    功,否则失败;

具体理论知识看视频

SpringSecurity自定义登录页面

在Securityconfig里配置自己的登录页,这个.build返回的对象就是DefaultSecurityFilterChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//配置springsecurity框架的一些行为(配置我们自己的登录页,不使用框架默认的登录页)
@Bean//安全过滤器链Bean
public SecurityFilterChain securityFilterChain(HttpSecurityhttpSecurity) throws Exception{//httpSecurity是方法参数注入Bean
//在springsecurity框架开发时,创建SecurityFilterChain这个Bean,不是直接newDefaultSecurityFilterChain
//return new DefaultSecurityFilterChain();这样不对

return httpSecurity

//配置我们自己的登录页,参数是T t,这里把T类型删去,交给jdk自动根据接口判断这个T是什么类型。formLogin里有个Customizer接口,这个需要自定义做定制化,这里把t改成了formLogin
.formLogin((formLogin) -> {
formLogin.loginPage()//定制登录页(Thymeleaf页面)
})

.build();
}

这里视频先介绍的Thymeleaf页面,需要学习的话直接看视频再整理笔记,这里直接跳到前后端分离

前后端分离

在前面的例子中,我们是返回到Thymeleaf页面,但如果是前后端分离开发,是不能返回一
个页面的,而应该是返回一个JSON;
Vue + springboot/spring security/mybatis (Nginx +Tomcat)无法共享sesssion
Thymeleaf+ springboot/spring security/mybatis(Tomcat)共享sesssion

这里提供了代码,登录不需要写controller,是框架提供的,只需要指定一个地址,到时候登录往这里提交就行,这里是/user/login

前后端分离的前端没有csrf,所以获取不到csrf的token,这个值只有后端跳到前端才可以拿到,所以要把他禁用

然后使用ajax会出现跨域问题,这里的跨域如果使用disaple是无法解决的,需要配置一些东西,使用configurationSource方法里面需要传个参数,这个对象可以通过方法参数注入,配置一个bean,在里面new这个对象,但这个对象是个接口,需要new他的实现类

跨域问题解决后还有个返回信息问题,当登录成功后不是跳转页面,而是跳一个handler,这个handler里的参数是个接口,需要写一个handler实现接口覆盖里面的抽象方法,就要新建一个类,然后注入进去

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
@Resource //注入handler
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Bean//配置跨域
public CorsConfigurationSource configurationSource(){
//return new CorsConfigurationSource;//这样不行
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();

//跨域配置
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedorigins(Arrays.asList("*"));//允许任何来源,http://localhost:10492/
corsConfiguration.setAllowedMethods(Arrays.asList("*"));//允许任何请求方法,post、get、put、delete
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));//允许任何的请求头(jwt)

//后面的方法是注册跨域配置,/**是代表任何配置
urlBasedCorsConfigurationSource.registerCorsConfiguration();
return new UrlBasedCorsConfigurationSource("/**", corsConfiguration);// /api/user这种
}

//配置springsecurity框架的一些行为(配置我们自己的登录页,不使用框架默认的登录页)
//但是当你配置了SecurityFilterChain这个Bean之后,Springsecurity框架的某些默认行为就弄丢了(失效了),此时你需要加回来(捡回来)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, CorsConfigurationSource configurationSource) throws Exception{
return httpSecurity

.formLogin((formLogin) -> {
//框架默认接收登录提交请求的地址是/login,但是我们把它给弄丢了,需要捡回来
formLogin.loginProcessingUrl("/user/login")//登录的账号密码往哪个地址提交
.successHandler(myAuthenticationSuccessHandler);
})

//把所有接口都会进行登录状态检查的默认行为,再加回来
.authorizeHttpRequests((authorizeHttpRequests) -> {
authorizeHttpRequests
.anyRequest().authenticated();//除了上面的特殊情况外,其他任何对后端接口的请求,都需要认证(登录)后才能访问
})

.csrf( (csrf)-> {
//禁用csrf跨站请求伪造,禁用之后,肯定就不安全了,有csrf网络攻击的风险,后续加入jwt是可以防御的
csrf.disaple();
})

.cors( (cors) -> {
//这个方法里面要传个参数
cors.configurationSource();
})

.build();
}

新建一个handler类

1
2
3
4
@Component
public class MyAuthenticatignSuccessHandler implements AuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authen

Spring Security密码加密和密码匹配

对于用户密码的保护,通常都会进行加密然后存放在数据库中;(基本常识)
目前密码加密MD5 和BCrypt比较流行,Spring Security默认是采用BCrypt;
SpringSecurity密码加密接口:PasswordEncoder;

准备一个配置类

1
2
3
4
5
6
7
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

我们只需要学习两个方法就可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class CaptchaLoginApplicationTests{

@Resource
private PasswordEncoder passwordEncoder;

@Test
void test01() {
String password = "aaa111";
String encodePassword = passwordEncoder.encode(password);//加密方法
System.out.println(encodePassword);

boolean match = passwordEncoder.matches(password,encodePassword);//密码匹配
System.out.println(match);

如果循环加密,查看控制台发现特点是:相同的字符串加密之后的结果都不一样,但是比较的时候是一样的

BCrypt加密原理

输入的明文密码比如aaa111,通过随机加盐salt(abcxyz….22位字符串)后再进行加密得到密文密码xxx
(version+salt+hash)版本号+盐值+哈希加密,然后存入数据库;

bcrypt(aaa111+22位随机的盐abcxyz)=密文

BCrypt匹配原理

系统在验证用户的密码时,需要从密文密码xXx中取出盐salt(22位),然后与用户页面输
入的password(aaa111)进行加密,把得到的结果与保存在数据库中的密文xxx进行比对,
如果一致才算验证通过;
明文:aa111
密文:$2a$10$ 9z08lUjY.Htp4xdWLT7TzO wrz4MGz4V7tt1m/61HdebDqR2m7Oj52
比较:bcrypt(aaa111+9z08lUjY.Htp4xdWLT7TzO)–>密文==上面这个密文;

其实最终还是比较密文

这个不能解密