单点登录
概念
在微服务或分布式架构下,用户的登录实现只需要登录一次,而登录信息在各个独立的模块下可以共用。
流程
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
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency>
<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: header: Authorization secret: abcxxxxxxxxxxxxxxxxxxx expireTime: 120
spring: redis: host: 127.0.0.1 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;
@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;
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
|
@Component public class TokenService { @Value("${token.header}") private String header;
@Value("${token.secret}") private String secret;
@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;
private String createToken(Map<String, Object> claims) { String token = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret).compact(); return token; }
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); }
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; }
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; }
public void refreshToken(LoginUser loginUser) { loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); String userKey = getTokenKey(loginUser.getToken()); redisTemplate.opsForValue().set(userKey, loginUser, expireTime, TimeUnit.MINUTES); }
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
|
@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
| service.interceptors.request.use(config => { const isToken = (config.headers || {}).isToken === false if (getToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getToken() } 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) }
|
前端能力还不足,只给出部分有关代码。