Spring Security主要解决了认证与授权的相关问题。
其需要添加的依赖是:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加了依赖的默认配置效果有:
1,所有请求都是必须通过认证的
如果未认证,同步请求将自动跳转到 /login,是框架自带的登录页,非跨域的异步请求将响应 403 错误
2,提供了默认的登录信息,用户名为 user,密码是启动项目是随机生成的,在启动日志中可以看到
(1)当登录成功后,会自动重定向到此前访问的URL
(2)当登录成功后,可以执行所有同步请求,所有异步的POST请求都暂时不可用
(3)可以通过 /logout 退出登录
BCrypt算法被设计为是一种慢速运算的算法,可以一定程度上避免或缓解密码被暴力破解(使用循环进行穷举的破解)。
当添加了Spring Security相关的依赖项后,此依赖项中将包含BCryptPasswordEncoder工具类,是一个使用BCrypt算法的密码编码器,它实现了PasswordEncoder接口,并重写了接口中的String encode(String rawPassword)方法,用于对密码原文进行编码(加密),及重写了boolean matches(String rawPassword, String encodedPassword)方法,用于验证密码原文与密文是否对应。
BCrypt算法会自动使用随机的盐值进行加密处理,所以,当反复对同一个原文进行加密处理,每次得到的密文都是不同的,但这并不影响验证密码!
默认情况下,Spring Security使用user作为用户名,使用随机的UUID作为密码来登录!
如果需要自行指定登录账号,需要自定义一个组件类,实现UserDetailService接口,此接口中定义了User Detail loadUserByUsername(String username),在处理认证时,当用户(使用者)输入了用户名,密码并提交,String Security就会自动使用用户在表单中输入的用户名来调用老大UserByUsername()方法,作为开发者,应该重写此方法,并根据用户名来返回匹配的UserDetails对象,此对象应该包含用户的相关信息,例如密码等,当Spring Security得到调用loadUserByUsername()返回的UserDetails对象后,会自动处理后续的认证过程,例如验证密码是否匹配等。
例如 ,在根包下创建security.UserDetailsServiceImpl类:
package cn.tedu.csmall.passport.security;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);
// 暂时使用模拟数据来处理登录认证,假设正确的用户名和密码分别是root和123456
if ("root".equals(s)) {
UserDetails userDetails = User.builder()
.username("root")
.password("$2a$10$nO7GEum8P27F8S0EGEHryel7m89opm/AMdaqMBk.qdsdIpE/SWFwe")
.accountExpired(false)
.accountLocked(false)
.disabled(false)
.authorities("权限标识") // 权限,注意,此方法的参数不可以为null,在不处理权限之前,可以写一个随意的字符串值
.build();
log.debug("即将向Spring Security返回UserDetails对象:{}", userDetails);
return userDetails;
}
log.debug("此用户名【{}】不存在,即将向Spring Security返回为null的UserDetails值", s);
return null;
}
}
注意!!!!
上述的密码是密文的,这只是一个例子,后面可以根据需要把用户名和密码改为数据库的数据,还有权限,此处只是写的随意一个字符串,后续有专门的权限处理!
再有,上述的UserDetailsServiceImpl中返回的UserDetails接口类型的对象是User类型的,此类型没有id属性,如果需要向JWT(后面会将)中封装id甚至其它属性,必须自定义类,继承自User或实现UserDetails接口,在自定义类中补充声明所需的属性,并在UserDetailsServiceImpl中返回自定义类的对象。
例如创建AdminDetails继承User
@Setter
@Getter
@EqualsAndHashCode
@ToString(callSuper = true)
public class AdminDetails extends User {
private Long id;
public AdminDetails(String username, String password, boolean enabled,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled,
true, true, true,
authorities);
}
}
在UserDetailsServiceImpl中,调整为返回AdminDetails类型的对象:
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
log.debug("从数据库查询与用户名【{}】匹配的管理员信息:{}", s, loginInfo);
if (loginInfo == null) {
log.debug("此用户名【{}】不存在,即将抛出异常");
String message = "登录失败,用户名不存在!";
throw new BadCredentialsException(message);
}
// ===== 以下是调整的内容 =====
List<GrantedAuthority> authorities = new ArrayList<>();
GrantedAuthority authority = new SimpleGrantedAuthority("权限标识");
authorities.add(authority);
AdminDetails adminDetails = new AdminDetails(
loginInfo.getUsername(), loginInfo.getPassword(),
loginInfo.getEnable() == 1, authorities);
adminDetails.setId(loginInfo.getId());
log.debug("即将向Spring Security返回UserDetails接口类型的对象:{}", adminDetails);
return adminDetails;
}
此处已经把id封装进去了,但是还没对权限进行处理,需要自行去数据库查询,多表联查找出用户的权限信息(此处就不详写了,就是数据库的基本查询),下面把查到的权限封装进AdminDetails,最终结果就能通过 adminDetails返回出来,用户信息都在这里面,下面的permission就是查找出来的权限。
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
log.debug("从数据库查询与用户名【{}】匹配的管理员信息:{}", s, loginInfo);
if (loginInfo == null) {
log.debug("此用户名【{}】不存在,即将抛出异常");
String message = "登录失败,用户名不存在!";
throw new BadCredentialsException(message);
}
// ===== 以下是此次调整的内容 =====
List<GrantedAuthority> authorities = new ArrayList<>();
for (String permission : loginInfo.getPermissions()) {
GrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
AdminDetails adminDetails = new AdminDetails(
loginInfo.getUsername(), loginInfo.getPassword(),
loginInfo.getEnable() == 1, authorities);
adminDetails.setId(loginInfo.getId());
}
Token机制是目前主流的取代Session用于服务器端识别客户端身份的机制。
Token就类似于现实生活中的“火车票”,当客户端向服务器端提交登录请求时,就类似于“买票”的过程,当登录成功后,服务器端会生成对应的Token并响应到客户端,则客户端就拿到了所需的“火车票”,在后续的访问中,客户端携带“火车票”即可,并且,服务器端有“验票”机制,能够根据客户端携带的“火车票”识别出客户端的身份。
JWT的全拼是:JSON Web Token,是使用JSON格式来组织多个属性于值,主要用于Web访问的Token。
JWT的本质就是只一个字符串,是通过算法进行编码后得到的结果。
在项目中,如果需要生成、解析JWT,需要添加以下依赖项:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
JWT主要由三部分组成:Header,Payload,Signature。
具体想要详细了解的话,可以参考以下地址:
JWT生成及解析网址
其中Header里面都是固定内容,
Payload里面放的就是主要内容,类似于id,用户名,邮箱,JWT过期时间这些用户信息的都可以放在里面,不同于Session的短时间过期,JWT是可以自定义过期时间的。
Signature里面保存的主要是随机的“盐”(就是一段自己写的随机的字母加数字加符号),这也是保证JWT唯一的凭证。
例如生成及解析如下:
package cn.tedu.csmall.passport;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTests {
String secretKey = "kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn";
//生成JWT
@Test
public void testGenerate() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 9527);
claims.put("username", "liucangsong");
claims.put("email", "[email protected]");
Date expirationDate = new Date(System.currentTimeMillis() + 10 * 60 * 1000);
System.out.println("过期时间:" + expirationDate);
String jwt = Jwts.builder()
// Header
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
// Payload
.setClaims(claims)
.setExpiration(expirationDate)
// Signature
.signWith(SignatureAlgorithm.HS256, secretKey)
// 整合
.compact();
System.out.println(jwt);
}
//解析JWT
@Test
public void testParse() {
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OTUyNywiZXhwIjoxNjY1NTY4Mjc1LCJlbWFpbCI6ImxpdWNhbmdzb25nQDE2My5jb20iLCJ1c2VybmFtZSI6ImxpdWNhbmdzb25nIn0.ESPNerLR2uBt1UtUhPwEU_71fcX_Ve-Td6X4Pjvegak";
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
Integer id = claims.get("id", Integer.class);
System.out.println("id=" + id);
String username = claims.get("username", String.class);
System.out.println("username=" + username);
String email = claims.get("email", String.class);
System.out.println("email=" + email);
String phone = claims.get("phone", String.class);
System.out.println("phone=" + phone);
}
}
注意!!!如果生成的JWT超过了过期时间的话,再解析是会出现ExpiredJwtException错误的:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-10-12T17:14:41Z. Current time: 2022-10-12T17:39:57Z, a difference of 1516448 milliseconds. Allowed clock skew: 0 milliseconds.
另外JWT是不安全的,因为在不知道secretKey的情况下,任何JWT都是可以解析出Header、Payload部分的,这2部分的数据并没有做任何加密处理,所以,如果JWT数据被暴露,则任何人都可以从中解析出Header、Payload中的数据!
至于JWT中的secretKey,及生成JWT时使用的算法,是用于对Header、Payload执行签名算法的,JWT中的Signature是用于验证JWT真伪的。
当然,如果你认为有必要的话,可以自行另外使用加密算法,将Payload中应该封装的数据先加密,再用于生成JWT!
另外,如果JWT数据被泄露,他人使用有效的JWT是可以正常使用的!所以,通常,在相对比较封闭的操作系统(例如智能手机的操作系统)中,JWT的有效时间可以设置得很长,但是,不太封闭的操作系统(例如PC端的操作系统)中,JWT的有效时间应该相对较短。
所以,在JWT时,需要注意:
1,根据你所需的安全性,来设置JWT的有效时间
2,不要在JWT中存放敏感数据,例如:手机号码、身份证号码、明文密码
3,如果一定要在JWT中存放敏感数据,应该自行使用加密算法处理过后再用于生成JWT