标题起的有点问题,因为统一登陆不一定实在同一个项目的不同模块,也可以是不同项目的不同模块。既可以是一个大的springcloud项目,也可以是多个分属于不同项目的springboot项目。
1.首先要理解何为统一登陆?
统一登陆可以通过一个具体的app来实现。下面是一张美团app的首页截图。
首页上面的美食,电影演出,酒店住宿,休闲娱乐,外卖五大模块都整合到这个app里。但是要知道美团之前就是美团外卖,后来的这些功能都是整合进来的。分属于不同的独立引用或者app中,肯定有自己独立的登陆。这些应用的不在一个模块甚至项目中。但是整合到美团app后,只要在app这层登陆后,这五大模块的登陆态就变成已登录状态,从而访问里面的信息不会应为再次登陆而被登陆拦截。一次登陆后,在这五个功能模块中都是已经登陆,便是统一登陆!
统一登陆绝不是美团app登陆后,在电脑网页版就也是登录态,这是不可能实现的能力。统一登陆是一种整合能力的表现。
2.实现统一登陆的技术选择
美团外卖的统一登陆或许有别的实现统一登陆的方式。上面只是以app的首页讲解何为统一登陆。这里的实现是我们公司的具体实现,和美团无关。
实现对请求的拦截处理有两种方式: implements javax.servlet.Filter
和 implements org.springframework.web.servlet.HandlerInterceptor
HandlerInterceptor在方法的preHandle,postHandle,afterCompletion实现拦截逻辑。Filter是在方法 doFilter()中实现拦截逻辑。
但是这里要有个清楚的认识Spring的Interceptor与Servlet的Filter区别点:
Filter作用于servlet容器,Interceptor作用于spring容器。一个请求过来经过二者的先后顺序是这样的。
Filter前处理 --> Interceptor前处理 --> action --> Interceptor后处理 --> Filter后处理
- 如果只是单纯的登录校验,将校验的代码放在Filter里面会更好,因为这样请求都不会到Spring里面就已经被Servlet拒绝了,这样可以更加好的保证Spring的安全。
- 如果项目需要根据权限去拦截,需要根据用户信息去查询数据库表。这就不可避免的要使用Spring的组件了,这种情况下就使用Interceptor处理好一些。
- 但是对于springboot项目,通过对filter注解
@Component
,即org.springframework.stereotype.Component
来让spring来管理。也是可以在filter中实现注具体service层的。
3.项目中的技术实现思路
对于springcloud项目的统一登陆大都放在zuul网关层实现,对登陆进行校验,拦截。对于zuul下的项目都需要登陆校验的项目来说是没有问题。但是我们的项目的登陆需求并不是都需要。有写还不需要登陆。大致是这样的。项目一,二和订单模块需要登陆,但是三方支付模块和open模块是不需要登陆的。而这些都在zuul网关的治理下。如果在zuul网关加上登陆校验,则不需要登陆的模块就会请求拦截,显示未登录进不了服务模块。
所以我们采用在各自模块上通过filter实现各自的登陆校验。而登陆要满足各个项目实现统一登陆。
技术实现简单说就是:jwt实现token回传给客户端,客户端的每次请求都带着token给后台。token+filter实现登陆态校验。token+session实现一次解析用户信息,项目层面更简单获取用户信息。
4.技术的具体实现
用户注册登陆接口(filter放行)返回生成的token,而后所有请求会被filter拦截进行token校验。
- 4.1 引入jwt的pom依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 4.2 通过jwt生成token的工具类
public class Constant {
public static final String TOKEN = "token";
public static final String EXPIRE_TIME = "expireTime";
public static final String USER_ID = "userId";
public static final String LOGIN_NAME = "loginName";
public static final String USER_NAME = "userName";
public static final String MOBILE = "mobile";
public static final String IP_ADDRESS = "ipAddress";
}
import com.server.constant.Constant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("Duplicates")
@Component
public class TokenComponent {
private Logger log =LoggerFactory.getLogger(TokenComponent.class);
public static final String CLAIM_KEY_USER = "userId";
public static final String CLAIM_KEY_CREATED = "created";
public static final String CLAIM_KEY_MOBILE = "mobile";
public static final String CLAIM_KEY_USERNAME = "userName";
public static final String CLAIM_KEY_LOGINNAME = "loginName";
private String secret = "secret";
/**
* 7 days
*/
private Long defaultExpiration = 604800L;
/**
* 生成token
* @param mobile 手机
* @param userId 用户ID
* @param userName 用户名称
* @param expiration 过期时间
* @return TOKEN
*/
public Map<String, String> generateToken(String userId, String mobile, String userName,String loginName, Long expiration) {
Map<String, Object> claims = new HashMap<>(5);
claims.put(CLAIM_KEY_USER, userId);
claims.put(CLAIM_KEY_MOBILE, mobile);
claims.put(CLAIM_KEY_CREATED, new Date());
claims.put(CLAIM_KEY_USERNAME, userName);
claims.put(CLAIM_KEY_LOGINNAME, loginName);
Date expirationDate;
if (expiration == null) {
expirationDate = new Date(System.currentTimeMillis() + defaultExpiration * 1000