服务认证中心
当用户从客户端发起用户认证请求的时候,服务认证中心作为统一的系统授权中心,承担了用户授权验证的作用,由于服务中心的授权操作,才能实现各级站点间的授权登录
客户端系统
客户端主要是承担各个站点的用户验证,他是用户登录的入口,但是他有不具有授权用户的能力(即不对外提供登录注册接口),当用户登录请求后(从服务端授权),客户端则用于与服务端的授权交互调用,并且对用户令牌进行缓存,只对含有缓存或者通过令牌请求登录的请求进行认证授权
站点的概念在SSO系统中由为重要(如阿里巴巴的淘宝和天猫),只有经过我认证的系统站点,才认定为合法,才会对SSO系统下的站点进行授权认证;系统采用了较为简单的XML配置文件手动配置解析,文件结构如下:
<?xml version="1.0" encoding="UTF-8"?>
<webSites>
<webSite id="1"
callbackUrl="http://localhost:81/">
</webSite>
<webSite id="2"
callbackUrl="http://localhost:82/">
</webSite>
</webSites>
webSite为站点的内容,我这里采用通过id的方式来区分站点,callbackUrl即认证成功后的回调地址。
xml文件的解析方式采用了DOM(JAXP Crimson解析器)的解析方式:
/**
* xml文件解析
* */
public class XmlParseUtil {
public static HashMap<Long, GxAppSite> siteXmlParse(File file) {
//创建DocumentBuilderFactory对象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
//解析xml文件并对象化
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
Document document = documentBuilder.parse(file);
NodeList webSites = document.getElementsByTagName("webSite");
//解析站点列表
HashMap<Long, GxAppSite> appSiteHashMap = pareseToAppSite(webSites);
return appSiteHashMap;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static HashMap<Long, GxAppSite> pareseToAppSite(NodeList webSites) {
HashMap<Long, GxAppSite> appSiteHashMap = new HashMap<>(16);
for (int i = 0; i < webSites.getLength(); i++) {
Node node = webSites.item(i);
//获取AppSite字段
Long id = Long.valueOf(node.getAttributes().getNamedItem("id").getTextContent());
String callbackUrl = node.getAttributes().getNamedItem("callbackUrl").getNodeValue();
AppSite appSite = new AppSite(id, callbackUrl);
appSiteHashMap.put(id, appSite);
}
return appSiteHashMap;
}
}
sso系统一定是区别于传统的session会话的模式的,他所采用的是需要满足Http的无状态的性质,因此就需要采用一种合理的方式去解决Http协议的无状态的性质。
而JWT则为一种加密验证模式,他是在用户登录授权成功后,返回给用户一个带有服务器秘钥的用户令牌(token),而在之后对有用户登录状态验证的服务请求时,用户携带这个经加密处理后的用户令牌,服务端通过解密操作完成用户的认证
/**
* 创建令牌
*
* @param appId 应用站点ID
* @return GxSessionInfo
*/
public synchronized static SessionInfo createToken(SessionInfo sessionInfo, Long appId) throws UnsupportedEncodingException {
Long systemTime = System.currentTimeMillis();
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
//JWT签名头部
Map<String, Object> header = new HashMap<>(2);
header.put("typ", TYPE);
header.put("alg", ALGORITHM);
//生成用户总token
Map<String, String> tokenMap = sessionInfo.getTokenMap();
if (tokenMap == null){
tokenMap = new HashMap<>(16);
}
//服务器端生成sso-server token
String split = "_";
if (tokenMap.get(USER_ID + split + sessionInfo.gUserId()) == null){
String serverToken = JWT.create()
.withClaim(USER_ID, sessionInfo.gUserId())//payload携带用户信息
.withClaim(USER_NAME,sessionInfo.gUserName())
.withHeader(header)
.withIssuer(ISSUER)//颁发令牌的用户
.withIssuedAt(new Date(systemTime))//记录生成时间
.sign(algorithm);//服务端秘钥
tokenMap.put(USER_ID + split + sessionInfo.gUserId(), serverToken);
}
//客户端端生成client-server token
if (appId != null && appId != 0L
&& tokenMap.get(APP_ID + split + appId) == null) {
//生成站点token,并保留其余站点token信息
String appToken = JWT.create()
.withClaim(USER_ID, sessionInfo.gUserId())
.withClaim(USER_NAME,sessionInfo.gUserName())
.withClaim(APP_ID, appId)
.withHeader(header)
.withIssuer(ISSUER)
.withIssuedAt(new Date(systemTime))
.sign(algorithm);
tokenMap.put(APP_ID + split + appId, appToken);
}
sessionInfo.setTokenMap(tokenMap);
return sessionInfo;
}
/**
* 验证令牌是否合法
*
* @param token 用户令牌
*/
public synchronized static long verifyToken(String token) throws UnsupportedEncodingException {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.build();
DecodedJWT jwt = verifier.verify(token);
Claim claim = jwt.getClaim(USER_ID);
if (!claim.isNull()) {
return claim.asLong();
}
return 0;
}
/**
* 根据标签反解令牌信息
*/
public static long decodeToken(String token, String name) {
if (StringUtils.isNotBlank(token)) {
try {
DecodedJWT jwt = JWT.decode(token);
Claim claim = jwt.getClaim(name);
if (!claim.isNull()) {
return claim.asLong();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return 0L;
}
/**
* 根据标签反解令牌信息
*/
public static String decodePayLoadToken(String token, String name) {
if (StringUtils.isNotBlank(token)) {
try {
DecodedJWT jwt = JWT.decode(token);
Claim claim = jwt.getClaim(name);
if (!claim.isNull()) {
return claim.asString();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return "";
}
用户首先通过客户端的登录入口,提交用户名密码,客户端发现用户未进行登录(客户端无token缓存或checkToken请求时失效),前端请求直接跳到sso-server的login接口请求:
public SsoInfo login(String username, String password, Long appId) throws Exception {
//动态表名登录用户
Example example = new Example(UserEntity.class);
example.and().andEqualTo("username", username);
example.setTableName(DynamicTableConfig.tableName);
List<GxUserEntity> userEntities = userMapper.selectByExample(example);
if (userEntities.size() == 0) {
throw new InvalidFieldException(null, "username", username, "用户不存在!");
}
if (userEntities.size() > 1) {
throw new InvalidFieldException(null, "username", username, "用户名解析异常!");
}
UserEntity user = userEntities.get(0);
if (!MD5Encrypt.valid(password, user.getPassword())) {
throw new InvalidFieldException(null, "password", password, "用户账号或登录密码错误!");
}
HashMap<Long, GxAppSite> appSiteHashMap = null;
if (appId != null && appId > 0) {
// 验证应用站点是否为配置站点
File file = ResourceUtils.getFile(fileUrl);
appSiteHashMap = XmlParseUtil.siteXmlParse(file);
if (appSiteHashMap!=null && !appSiteHashMap.containsKey(appId)) {
//appId不存在则站点非法
throw new InvalidFieldException(null, "appId", String.valueOf(appId), "应用站点不合法!");
}
}
//建立缓存对象
SessionInfo sessionInfo = new SessionInfo();
//设置用户信息
sessionInfo.sUserName(user.getUsername());
sessionInfo.sUserId(user.getId());
sessionInfo.setLoginTime(System.currentTimeMillis());
//生成或者更新token
TokenHelper.createToken(sessionInfo, appId);
//生成服务端缓存
getSessionUser().put(user.getId(), sessionInfo);
//构造token返回对象
SsoInfo ssoInfo = getSsoInfoResult(sessionInfo, appId, appSiteHashMap);
//登录成功后定制化业务
Map<String, AbstarctAuthListener> listenerManager = ListenerBeanContainer.getListenerManager();
if (listenerManager.size() > 0) {
AuthUser authUser = new AuthUser(sessionInfo.gUserId(),sessionInfo.gUserName());
for (Map.Entry<String, AbstarctAuthListener> entry : listenerManager.entrySet()) {
entry.getValue().afterLogin(authUser);
}
}
logger.info("用户:" + user.getId() + ",于:" + DateUtil.getNow() + "登录认证中心成功!");
return ssoInfo;
}
认证中心完成授权登录后,将服务端生成的serverToken和clientToken一并返回给用户,并在服务端缓存(缓存策略可增加redis实现),客户端在获取到用户令牌后,前端发送验证返回token是否合法(增加token认证安全性,并将clientToken缓存到浏览器),之后的用户请求则只与站点交互,降低认证中心压力
public SsoInfo loginToken(String token, Long appId) throws Exception {
if (StringUtils.isBlank(token)) {
throw new GxMissingFieldException(null, "token");
}
//判断缓存是否含有token
SessionInfo sessionInfo = getSessionUser().check(token);
if (sessionInfo == null) {
throw new InvalidFieldException(null, "token", token, "用户认证失效!");
}
HashMap<Long, GxAppSite> appSiteHashMap = new HashMap<>(16);
if (appId != null && appId > 0) {
appSiteHashMap = checkAppInfo(appId);
}
//生成新的token或刷新token,刷新时效
SessionInfo sessionInfo = GxTokenHelper.createToken(sessionInfo, appId);
getSessionUser().put(sessionInfo.gUserId(), gxSessionInfo);
return getSsoInfoResult(gxSessionInfo, appId, appSiteHashMap);
}
客户端请求过loginToken验证令牌合法性时,通过远程接口调用去请求服务端的checkToken接口(只从缓存中拿取是否有当前token)
@Override
public AuthUser checkToken(String token) throws Exception {
SessionInfo sessionInfo = getSessionUser().check(token);
if (sessionInfo == null) {
throw new InvalidFieldException(null, "token", token, "用户认证失效!");
}
return sessionInfo.getUser();
}
这样就完成了一次用户登录授权请求。
当下一个站点B登录时,浏览器发现已经有认证中心的serverToken的令牌时,便携带serverToken与站点的AppId去请求服务端的loginToken接口,生成一个新的clientTokenB返回客户端,站点B则再去请求客户端loginToken接口来缓存clientTokenB到站点客户端
这个过程会有些繁琐,容易让人迷失,需要去细细品味整个授权登录的流程