Spring Security是用于解决认证与授权的框架。
创建spring项目,添加依赖
<!-- Spring Boot Security:处理认证与授权 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
如:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 父级项目 -->
<parent>
<groupId>cn.tedu</groupId>
<artifactId>csmall-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- 当前项目的信息 -->
<groupId>cn.tedu</groupId>
<artifactId>csmall-passport</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 当前项目需要使用的依赖项 -->
<dependencies>
<!-- Spring Boot Web:支持Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Security:处理认证与授权 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Test:测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
调整完成后,即可启动项目,在启动的日志中,可以看到类似以下内容:
Using generated security password: 2abb9119-b5bb-4de9-8584-9f893e4a5a92
Spring Security有默认登录的账号和密码(以上提示的值),密码是随机的,每次启动项目都会不同。
Spring Security默认要求所有的请求都是必须先登录才允许的访问,可以使用默认的用户名user
和自动生成的随机密码来登录。在测试登录时,在浏览器访问当前主机的任意网址都可以(包括不存在的资源),会自动跳转到登录页(是由Spring Security提供的,默认的URL是:http://localhost:8080/login),当登录成功后,会自动跳转到此前访问的URL(跳转登录页之前的URL),另外,还可以通过 http://localhost:8080/logout 退出登录。
Spring Security的依赖项中包括了Bcrypt算法的工具类,Bcrypt是一款非常优秀的密码加密工具,适用于对需要存储下来的密码进行加密处理。
public class BcryptPasswordEncoderTests {
private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Test
public void testEncode() {
// 原文相同的情况,每次加密得到的密文都不同
for (int i = 0; i < 10; i++) {
String rawPassword = "123456";
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("rawPassword = " + rawPassword);
System.out.println("encodedPassword = " + encodedPassword);
}
// rawPassword = 123456
// encodedPassword = $2a$10$HWuJ9WgPazrwg9.isaae4u7XdP7ohH7LetDwdlTWuPC4ZAvG.Uc7W
// encodedPassword = $2a$10$rOwgZMpDvZ3Kn7CxHWiEbeC6bQMGtfX.VYc9DCzx9BxkWymX6FbrS
// encodedPassword = $2a$10$H8ehVGsZx89lSVHwBVI37OkxWm8LXei4T1o5of82Hwc1rD0Yauhky
// encodedPassword = $2a$10$meBbCiHZBcYn7zMrZ4fPd.hizrsiZhAu8tmDk.P8QJcCzSQGhXSvq
// encodedPassword = $2a$10$bIRyvV29aoeJLo6hh1M.yOvKoOud5kC7AXDMSUW4tF/DlcG0bLj9C
// encodedPassword = $2a$10$eq5BuoAiQ6Uo0.TOPZOFPuRNlPl3t2GoTlaFoYfBu3/Bo3tLzx.v2
// encodedPassword = $2a$10$DhTSwQfNdqrGgHRmILmNLeV0jt3ZXL435xz0fwyZ315ciI5AuI5gi
// encodedPassword = $2a$10$T.8/ISoLOdreEEkp4py36O0ZYfihDbdHDuIElZVF3uEgMOX.8sPcK
// encodedPassword = $2a$10$hI4wweFOGJ7FMduSmcjNBexbKFOjYMWl8hkug0n0k1LNR5vEyhhMW
// encodedPassword = $2a$10$b4ztMI6tWoiJuoDYKwr7DOywsPkkCdvDxbPfmEsLdp11NdABS7wyy
}
@Test
public void testMatches() {
String rawPassword = "123456";
String encodedPassword = "$2a$10$hI4wweFOGJ7FMduSmCjNBexbKFOjYMWl8hkug0n0k1LNR5vEyhhMW";
boolean matchResult = passwordEncoder.matches(rawPassword, encodedPassword);
System.out.println("match result : " + matchResult);
}
}
如果要使得Spring Security能使用数据库中的信息(数据库中的用户名与密码)来验证用户身份(认证),首先,必须实现“根据用户名查询此用户的登录信息(应该包括权限信息)”的查询功能,要实现此查询,需要执行的SQL语句大致是:
select
ams_admin.id,
ams_admin.username,
ams_admin.password,
ams_admin.is_enable,
ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin.id = ams_admin_role.admin_id
left join ams_role_permission on ams_admin_role.role_id = ams_role_permission.role_id
left join ams_permission on ams_role_permission.permission_id = ams_permission.id
where username='root';
Spring Security的认证机制中包含:当客户端提交登录后,会自动调用UserDetailsService
接口(Spring Security定义的)的实现类对象中的UserDetails loadUserByUsername(String username)
方法(根据用户名加载用户数据),将得到UserDetails
类型的对象,此对象中应该至少包括此用户名对应的密码、权限等信息,接下来,Spring Security会自动完成密码的对比,并确定此次客户端提交的信息是否允许登录!类似于:
// Spring Security的行为
UserDetails userDetails = userDetailsService.loadUserByUsername("chengheng");
// Spring Security将从userDetails中获取密码,用于验证客户端提交的密码,判断是否匹配
所以,要实现Spring Security通过数据库的数据来验证用户名与密码(而不是采用默认的user
用户名和随机的密码),则在cn.tedu.csmall.passport
包下创建security.UserDetailsServiceImpl
类,实现UserDetailsService
接口,并重写接口中的抽象方法:
package cn.tedu.csmall.passport.security;
import cn.tedu.csmall.passport.mapper.AdminMapper;
import cn.tedu.csmall.pojo.vo.AdminLoginVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
System.out.println("根据用户名查询尝试登录的管理员信息,用户名=" + s);
AdminLoginVO admin = adminMapper.getLoginInfoByUsername(s);
System.out.println("通过持久层进行查询,结果=" + admin);
if (admin == null) {
System.out.println("根据用户名没有查询到有效的管理员数据,将抛出异常");
throw new BadCredentialsException("登录失败,用户名不存在!");
}
System.out.println("查询到匹配的管理员数据,需要将此数据转换为UserDetails并返回");
UserDetails userDetails = User.builder()
.username(admin.getUsername())
.password(admin.getPassword())
.accountExpired(false)
.accountLocked(false)
.disabled(admin.getIsEnable() != 1)
.credentialsExpired(false)
.authorities(admin.getPermissions().toArray(new String[] {}))
.build();
System.out.println("转换得到UserDetails=" + userDetails);
return userDetails;
}
}
完成后,再配置密码加密器即可:
package cn.tedu.csmall.passport.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
重启项目,可以发现在启动过程中不再生成随机的密码值,在浏览器上访问此项目的任何URL,进入登录页,即可使用数据库中的管理员数据进行登录。
在Spring Security,默认使用Session机制存储成功登录的用户信息(因为HTTP协议是无状态协议,并不保存客户端的任何信息,所以,同一个客户端的多次访问,对于服务器而言,等效于多个不同的客户端各访问一次,为了保存用户信息,使得服务器端能够识别客户端的身份,必须采取某种机制),当下,更推荐使用Token或相关技术(例如JWT)来解决识别用户身份的问题。
JWT = JSON Web Token,它是通过JSON格式组织必要的数据,将数据记录在票据(Token)上,并且,结合一定的算法,使得这些数据会被加密,然后在网络上传输,服务器端收到此数据后,会先对此数据进行解密,从而得到票据上记录的数据(JSON数据),从而识别用户的身份,或者处理相关的数据。
其实,在客户端第1次访问服务器端时,是“空着手”访问的,不会携带任何票据数据,当服务器进行响应时,会将JWT响应到客户端,客户端从第2次访问开始,每次都应该携带JWT发起请求,则服务器都会收到请求中的JWT并进行处理。
要使用JWT,需要添加相关的依赖项,可以实现生成JWT、解析JWT的框架较多,目前,主流的JWT框架可以是jjwt
:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>