单点登录前文:
使用Spring Security实现单点登录_寒風冷度夜雨的博客-CSDN博客
上文我们说到重启项目,可以发现在启动过程中不再出现随机密码,然后访问浏览器,进入登录页,使用管理员用户名密码即可登录.
一.使用JWT形式保存用户登录信息
由于Spring Security默认使用的是Session机制来保存用户信息的,但是在我们的微服务架构下,这种机制是有弊端的,详情参见:
Spring Security单点登录解析_寒風冷度夜雨的博客-CSDN博客
所以,我们在微服务架构下要实现单点登录就必须使用另外一种手段来保存用户信息,这种手段就是JWT,全称:JSON Web Token.
这个Token就好比我们去游乐园,需要买票才能进去,进去之后我们要玩过山车之类的项目的时候会检查我们的门票是一个道理,Token就是票据的意思,前端访问我们服务器的时候,通过输入用户名密码,我们就能生成一个JWT数据响应给前端,这个数据里面包含了用户名,密码,以及权限等基本信息,用户下次访问别的模块的时候,只需要在请求头中携带了JWT数据,我们服务器在接收请求的时候解析一下,就可以判断出是否是登录过的用户了.
那么如何使用JWT呢?实现JWT的框架有很多,主流的框架此处选择的是jjwt框架,使用该框架,我们就需要导入依赖项:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
在我们的根项目上要使用该依赖,并进行版本控制,然后在子项目模块中我们也要添加该依赖项,以便我们子项目来生成或者解析JWT数据.
导入了该依赖项之后,我们可以先来对这个依赖项进行一个测试:
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest
public class TestJwt {
//1.随机定义一个密钥
String secretKey = "lakgjelaiutoaglkajflakglagh";//这个key可以是一个随机的一个字符串
//2.测试生成jwt
@Test
void testCreateJwt(){
//jwt的组成部分:Header(头)+PayLoad(负载)+Signature(签名)
//3.自定义数据
Map<String,Object> claims = new HashMap<>();
claims.put("id",9527);
claims.put("name","周星星");
//4.生成jwt数据
//Header:指定算法和当前数据类型
String jwt = Jwts.builder()
.setHeaderParam(Header.CONTENT_TYPE,"HS256") //Header部分
.setHeaderParam(Header.TYPE,Header.JWT_TYPE)
.setClaims(claims) //自定义数据(包含claims和过期时间)
.setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
.signWith(SignatureAlgorithm.HS256,secretKey) //签名,包含指定算法和密钥
.compact(); //打包生成
System.out.println(jwt);
//eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoi5ZGo5pif5pifIiwiaWQiOjk1MjcsImV4cCI6MTY1NzAzMDMwM30.-xp7RKbskefaqKD3dChG2aRaOWR2jV8X2nkKYu9zWck
}
}
通过上面的测试,我们可以看到已经生成了一个JWT数据:
eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoi5ZGo5pif5pifIiwiaWQiOjk1MjcsImV4cCI6MTY1NzAzMDMwM30.-xp7RKbskefaqKD3dChG2aRaOWR2jV8X2nkKYu9zWck
注意:我们在上面代码中设置了该Token的过期时间为30秒,因此,一旦超过这个过期时间,这个jwt数据就失效了!!!
这是通过一系列算法生成的,里面包含了用户的一些信息,就是我们自定的数据:id=9527,name=周星星,然后我们再来解析一下这个JWT数据看看:
@Test
void parseJwt(){
//1.将之前生成的jwt数据复制过来
String jwt = "eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoi5ZGo5pif5pifIiwiaWQiOjk1MjcsImV4cCI6MTY1NzAzMDMwM30.-xp7RKbskefaqKD3dChG2aRaOWR2jV8X2nkKYu9zWck";
//2.进行解析,返回一个Claims类型
//setSigningKey(secretKey):设置当初生成jwt时的密钥
//parseClaimsJws(jwt):解析jwt数据
//getBody():获取解析的内容
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
//3.从claims类型的数据中取出我们当初存入的自定义数据
Object id = claims.get("id");
Object name = claims.get("name");
System.out.println("id=:" + id);
System.out.println("name=:" + name);
}
可以看到最终得到的结果:
特别要说明的是:如果运行程序的时候抛出了以下异常,则分别对应了某些特定的情况:
1.数据过期ExpiredJwtException:该异常是当我们生成的jwt数据超过设定的过期时间的时候抛出的一个异常,只需要重新生成一遍jwt,然后再解析就可以解决
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-06-16T15:47:57Z. Current time: 2022-06-16T16:08:32Z, a difference of 1235869 milliseconds. Allowed clock skew: 0 milliseconds.
2.数据错误MalformedJwtException:该异常时我们生成的jwt数据在解析的时候,里面的内容发生了错误,例如多了一个字符或者少了一个字符,解决方案就是将之前生成的jwt数据重新复制一份进行解析,复制过程中千万不能有错漏哦!
io.jsonwebtoken.MalformedJwtException: Unable to read JSON value: {"cty"�"HS256","typ":"JWT","alg":"HS256"}
3.密钥不一致错误SignatureException:该异常是我们生成jwt和解析jwt的时候输入的密钥不一致导致的,只需要将密钥变成一致的即可解决
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
二.在Spring Security中使用jwt
那么如何在Spring Security管理的项目中使用我们的jwt数据呢?
首先我们要明确,Spring Security默认使用的是Session机制的,我们需要改变这种机制需要做到对之前的代码进行一个重构
1.需要一个AuthenticationManager对象交给Spring容器来管理,进行自动装配,我们就需要在Spring Security的配置类:SpringConfiguration中继承WebSecurityConfigurerAdapter类,这是一个抽象类,我们需要重写里面的AuthenticationManager方法,并添加@Bean注解
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
直接重写父类方法即可,不需要写任何代码
2.创建用于用户登录使用的AdminLoginDTO类,里面包含登录时需要提交的用户名和密码
import lombok.Data;
import java.io.Serializable;
@Data
public class AdminLoginDTO implements Serializable {
private String username;
private String password;
}
3.创建IAdminService接口,添加用户登录的抽象方法:
import cn.tedu.sso.pojo.dto.AdminLoginDTO;
public interface IAdminService {
//用户登录
String login(AdminLoginDTO adminLoginDTO);
}
4.创建AdminServiceImpl实现类,用于实现以上接口,编写具体的登录业务,在实现过程中,调用AuthenticationManager实现认证,生成并返回jwt数据:
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AuthenticationManager authenticationManager;
private String secretKey = "alkgjalejioatglkajl";
@Override
public String login(AdminLoginDTO adminLoginDTO) {
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(),adminLoginDTO.getPassword()
);
authenticationManager.authenticate(authentication);
String jwt = "This is a jwt"; //临时生成的数据
return jwt;
}
5.创建AdminController类,在类中处理登录请求:
@RestController
@RequestMapping(value = "/admins",produces = "application/json;charset=utf-8")
public class AdminController {
@Autowired
private IAdminService adminService;
@RequestMapping("/login")
public String login(AdminLoginDTO adminLoginDTO){
String jwt = adminService.login(adminLoginDTO);
return jwt;
}
}
6.在SpringConfiguration配置类中配置Spring Security,对特定的请求放行(默认所有的请求都必须先登录)
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁止防跨域攻击
http.csrf().disable();
//配置白名单
String[] urls = {
"/admins/login"
};
// 配置各请求路径的认证与授权
http.authorizeRequests() // 请求需要授权才可以访问
.antMatchers(urls) // 配置白名单内的请求路径放行
.permitAll() // 允许直接访问
.anyRequest() // 匹配除了以上配置的其他请求
.authenticated(); // 都需要认证
}
该方法也是重写的父类WebSecurityConfigurerAdapter中的方法,专门用于设置放行请求的白名单设置
三.测试登录
以上全部完成后,启动项目,打开浏览器,在地址栏中输入用户名和密码信息来提交请求给服务器:
http://localhost:8080/admins/login?username=root&password=123456
可以得到如下结果:
测试成功!