单点登录

概念

在微服务或分布式架构下,用户的登录实现只需要登录一次,而登录信息在各个独立的模块下可以共用。

流程

1、前端登录,将登录信息传给后端。

2、后端验证登录信息后根据登录信息生成token,将用户信息、token存储于redis缓存中,并将token返还给前端。

3、前端拿到token后间其存储起来(cookie),并在每一次向后端发出请求时给请求头加上token。

4、后端拦截请求,获取请求头里的token,验证token的有效性。

实现

新建前后端项目

不做赘述

后端

pom(子模块)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- SpringWeb模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>

<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>

<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

yaml

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
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcxxxxxxxxxxxxxxxxxxx
# 令牌有效期(默认30分钟)
expireTime: 120

spring:
# redis 配置
redis:
# 地址
host: 127.0.0.1
# 端口,默认为6379
port: 6379
# 密码
password: 123456
# 连接超时时间
timeout: 30s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
database: 11

实体类

登录实体类不做赘述

登录模块

controller

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
@RestController
public class LoginController
{
@Autowired
private LoginService loginService;

@Autowired
private TokenService tokenService;

/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public HashMap login(@RequestBody LoginBody loginBody)
{
HashMap<String,Object> ajax = new HashMap<>();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid());
ajax.put("token", token);
return ajax;
}
}

LoginService

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
/**
* 登录校验方法
*/
@Component
public class LoginService
{
@Autowired
private TokenService tokenService;

@Autowired
public RedisTemplate redisTemplate;

/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public String login(String username, String password, String code, String uuid)
{

String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisTemplate.getStringSerializer(verifyKey);
redisTemplate.delete(verifyKey);
if (captcha == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();
}

// 用户验证
/*
用户验证模块自己写,爱怎么写怎么写,数据库里查出来验证也行
*/

return tokenService.createToken(loginUser);
}
}

TokenService

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* token验证处理
*/
@Component
public class TokenService
{
// 令牌自定义标识
@Value("${token.header}")
private String header;

// 令牌秘钥
@Value("${token.secret}")
private String secret;

// 令牌有效期(默认30分钟)
@Value("${token.expireTime}")
private int expireTime;

protected static final long MILLIS_SECOND = 1000;

protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

@Autowired
private RedisTemplate redisTemplate;

/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}

/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser)
{
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}

/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(userKey);
}
return null;
}

/**
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}

/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisTemplate.opsForValue().set(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}

/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser
* @return 令牌
*/
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
{
refreshToken(loginUser);
}
}
}

请求过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* token过滤器 验证token有效性
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}

若模块间需要相互调用,需要做token传递,这里以RestTemplate为例。

1
2
3
4
5
6
7
8
9
10
@Component
public class TokenInterceptor implements ClientHttpRequestInterceptor {

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add("Authorization", "你的token,可以登录时预存在redis里这里取出来");
return execution.execute(request, body);
}
}

前端

登陆事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},

request.js拦截器

1
2
3
4
5
6
7
8
9
10
11
12
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})

token.js

1
2
3
4
5
6
7
8
9
10
11
export function getToken() {
return Cookies.get(TokenKey)
}

export function setToken(token) {
return Cookies.set(TokenKey, token)
}

export function removeToken() {
return Cookies.remove(TokenKey)
}

前端能力还不足,只给出部分有关代码。