什么是 SSO(单点登录)
SSO
英文全称 Single Sign On
,单点登录。SSO 是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
单点登录流程
单点登录大致流程如下所示:
单点登录详细流程:
首先在前端点击登录子系统的时候(主系统已经完成账号和密码登录),主系统会给前端在页面的地址栏上面给出code
值(我感觉不是很安全,但就是设计成这样),code
值是随机生成的,并且有时效性(过段时间就无效了)。
后端代码里面会直接接收这样的code值:
之后根据文档,把code值和其他在配置文件配置的固定值封装在一起并且传给主系统以获取accessToken
以及其他的信息(用户登录信息等),代码如下:
// 使用授权码code换取accessToken
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("code", code));
params.add(new BasicNameValuePair("client_id", Configuration.getClientId()));
params.add(new BasicNameValuePair("client_secret", Configuration.getClientSecret()));
params.add(new BasicNameValuePair("auth_type", Configuration.getGrantType()));
params.add(new BasicNameValuePair("redirect_uri", Configuration.getRedirectUri()));
JSONObject result = HttpClientUtils.post(Configuration.getClientUrl(), params);
log.info("使用授权码code换取accessToken URL:{},params:{}",Configuration.getClientUrl(),JSON.toJSONString(params));
接下来把获取的信息遍历并且封装起来,最后需要把这些信息都传给前端:
// 一般用HashMap传给前端
Map<String,Object> res = new HashMap<String,Object>();
// 直接用迭代器遍历JSONObject对象result
Iterator iter = result.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
res.put(entry.getKey().toString(), entry.getValue().toString());
}
如果主系统传给我的result
不为空,就直接开始解析这个accessToken
以获取单点登录的用户信息,子系统可以直接解析这个accessToken
:
String key = Configuration.getJwtKey();
Claims claims = Jwts.parser().setSigningKey(generalKey(key)).parseClaimsJws(result.getString("access_token")).getBody();
JWTInfo info = new JWTInfo(claims);
log.info("解析access_token:{}", info);
log.info("state:" + state);
log.info("用户信息:{}", info);
解析accessToken
之后,就开始走子系统(我这个系统)的登录逻辑。
如果不存在时,则直接添加用户(和直接新增一个用户一样的逻辑):
// 本地登陆逻辑
User user = UserService.findByUserName(info.getUsername());
if (user==null){
log.info("当前用户在系统不存在时 新增用户");
// 当前用户在系统不存在时 新增用户
User User=new User();
// 默认密码是当前账号名
User.setPassword(info.getUsername());
User.setUsername(info.getUsername());
User.setTruename(info.getName());
User.setNickname(info.getUsername());
User.setTenantid(tenantid_default);
User.setSchemaname(crtenant + tenantid_default.toString());
// 设置用户状态
User.setStatus(1);
// todo 给新增用户设置默认的权限
log.info("新增用户,参数:{}",User);
user = UserService.addUser(User);
log.info("新增用户返回的结果:{}",user==null?"null":JSON.toJSONString(user));
}
如果新增完用户或者本地本来就有这个用户,就继续往下走,先把这个用户放入登录用户的上下文(当前线程ThreadLocal
,每个用户都会有单独的线程,以用户名作为当前线程的唯一标识,以后都从这里取用户名),然后再新增一个JWT Token
(本系统新增的token
信息,目的是为了传给前端,前端以后都用这个token
信息来获取数据),并且将这个token
信息放入res
中,最后通过mergeRightValues
来整合用户的权限信息:
log.info("使用用户名密码登录 username:{},password:{}",user.getUsername()==null?"null":user.getUsername(),user.getPassword()==null?"null":user.getPassword());
// 放入当前登录用户上下文
UserContext.setCurrentUserName(user.getUsername());
String token = JWTUtils.createJWT("username", user.getUsername(), user.getUsername(), expire * 1000);
res.put("Token", token);
String rightvalues = authService.mergeRightValues(user);
接着,通过access_token
来获取主系统的用户信息和token
信息:
// 获取主系统的token
String access_token = result.getString("access_token");
List<NameValuePair> queryParams = new ArrayList<>();
queryParams.add(new BasicNameValuePair("token", access_token));
FrontUserDto frontUserDto = HttpClientUtils.getForxxx("http://172.xxx.xxx:8080/xxx/xxx/xxx/front/info", queryParams, access_token);
log.info("获取主系统的信息:frontUserDto:{}",frontUserDto);
res.put("userinfo", frontUserDto);
接下来把用户的媒体权限信息保存到redis
中,之后前端每次从后端获取数据的时候都会从redis
里面直接获取用户的媒体权限信息来确保信息的安全性:
// 把用户的权限缓存到redis中
List<PermissionInfoDto> copyright = frontUserDto.getElements().stream().filter(permissionInfoDto -> permissionInfoDto.getCode().contains("COPYRIGHT")).collect(Collectors.toList());
String userMediaIds = loginUserService.getMediaIdsBasedMediaName(copyright);
if (StringUtils.isEmpty(userMediaIds)){
return new CallBackMessage(result);
}
redisTemplate.opsForValue().set("userMedia-"+ frontUserDto.getUsername(),userMediaIds);
接下来解析之前的权限信息rightvalues
,解析成为权限ID值rightIds
(这个ID值具有检索用户权限的作用,直接传给前端,之后前端会根据这些权限ID值来获取数据,需要注意的是,这个和前面的媒体权限ID不是一样的):
String rightIds = authService.getRightIdsByRightvalues(rightvalues);
log.info("defaultRightIds:{}",defaultRightIds==null?"null":defaultRightIds);
log.info("rightIds:{}",rightIds==null?"null":rightIds);
//当获取到权限为空时 使用默认权限防止前端报错
if (StringUtils.isBlank(rightIds)){
rightIds=StringUtils.join(defaultRightIds.split(","), "_");
}else {
rightIds = StringUtils.join(rightIds.split(","), "_");
}
log.info("rightIds:{}",rightIds==null?"null":rightIds);
log.info("token:{}",token==null?"null":token);
res.put("rightIds", rightIds);
接着将前面通过code
获取的refresh_token
缓存到redis
中,之后注销登录的时候需要通过获取refresh_token
来确认这个用户以此来注销用户:
//将refresh_token缓存到redis
String refreshToken=result.getString("refresh_token");
if (StringUtils.isNotBlank(refreshToken)&&StringUtils.isNotBlank(token)){
String redisKey="data:center:auth:logout:";
stringRedisTemplate.opsForValue().set(redisKey+token,refreshToken,1, TimeUnit.DAYS);
}
最后,把登录日志存入数据库,并且返回res
给前端:
//记录登录日志
MySchema mySchema = new MySchema();
mySchema.setSchemaname(user.getSchemaname());
mySchema.setTenantid(user.getTenantid());
MySchemaHolder.setCurrentMySchema(mySchema);
sysLogService.addSafetyLog(user, request);
return new CallBackMessage(res);
全部代码
@RequestMapping(value = "/token", method = RequestMethod.GET)
public CallBackMessage login(String code, String state, HttpServletResponse response,HttpServletRequest request) throws IOException {
Map<String,Object> res = new HashMap<String,Object>();
// 使用授权码code换取accessToken
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("code", code));
params.add(new BasicNameValuePair("client_id", Configuration.getClientId()));
params.add(new BasicNameValuePair("client_secret", Configuration.getClientSecret()));
params.add(new BasicNameValuePair("auth_type", Configuration.getGrantType()));
params.add(new BasicNameValuePair("redirect_uri", Configuration.getRedirectUri()));
JSONObject result = HttpClientUtils.post(Configuration.getClientUrl(), params);
log.info("使用授权码code换取accessToken URL:{},params:{}",Configuration.getClientUrl(),JSON.toJSONString(params));
Iterator iter = result.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
res.put(entry.getKey().toString(), entry.getValue().toString());
}
System.out.println(res);
if (result != null) {
log.info("使用授权码code换取accessToken,返回结果:{}",result.toJSONString());
try {
String key = Configuration.getJwtKey();
Claims claims = Jwts.parser().setSigningKey(generalKey(key)).parseClaimsJws(result.getString("access_token")).getBody();
JWTInfo info = new JWTInfo(claims);
log.info("解析access_token:{}", info);
log.info("state:" + state);
log.info("用户信息:{}", info);
// 本地登陆逻辑
User user = UserService.findByUserName(info.getUsername());
if (user==null){
log.info("当前用户在系统不存在时 新增用户");
// 当前用户在系统不存在时 新增用户
User User=new User();
// 默认密码是当前账号名
User.setPassword(info.getUsername());
User.setUsername(info.getUsername());
User.setTruename(info.getName());
User.setNickname(info.getUsername());
User.setTenantid(tenantid_default);
User.setSchemaname(crtenant+tenantid_default.toString());
// 设置用户状态
User.setStatus(1);
// todo 给新增用户设置默认的权限
log.info("新增用户,参数:{}",User);
user = UserService.addUser(User);
log.info("新增用户返回的结果:{}",user==null?"null":JSON.toJSONString(user));
}
log.info("使用用户名密码登录 username:{},password:{}",user.getUsername()==null?"null":user.getUsername(),user.getPassword()==null?"null":user.getPassword());
// 放入当前登录用户上下文
UserContext.setCurrentUserName(user.getUsername());
String token = JWTUtils.createJWT("username", user.getUsername(), user.getUsername(), expire * 1000);
res.put("copyRightToken", token);
String rightvalues = authService.mergeRightValues(user);
// 获取主系统的token
String access_token = result.getString("access_token");
List<NameValuePair> queryParams = new ArrayList<>();
queryParams.add(new BasicNameValuePair("token", access_token));
FrontUserDto frontUserDto = HttpClientUtils.getForSzxm("http://xxxx.xxxx.xxx/xxx/xxx/xxx/front/info", queryParams, access_token);
log.info("获取主系统的信息:frontUserDto:{}",frontUserDto);
res.put("userinfo", frontUserDto);
// 把用户的权限缓存到redis中
List<PermissionInfoDto> copyright = frontUserDto.getElements().stream().filter(permissionInfoDto -> permissionInfoDto.getCode().contains("COPYRIGHT")).collect(Collectors.toList());
String userMediaIds = loginUserService.getMediaIdsBasedMediaName(copyright);
if (StringUtils.isEmpty(userMediaIds)){
return new CallBackMessage(result);
}
redisTemplate.opsForValue().set("userMedia-"+ frontUserDto.getUsername(),userMediaIds);
// String rightIds = "";
String rightIds = authService.getRightIdsByRightvalues(rightvalues);
log.info("defaultRightIds:{}",defaultRightIds==null?"null":defaultRightIds);
log.info("rightIds:{}",rightIds==null?"null":rightIds);
// 当获取到权限为空时 使用默认权限防止前端报错
if (StringUtils.isBlank(rightIds)){
rightIds=StringUtils.join(defaultRightIds.split(","), "_");
}else {
rightIds = StringUtils.join(rightIds.split(","), "_");
}
log.info("rightIds:{}",rightIds==null?"null":rightIds);
log.info("token:{}",token==null?"null":token);
res.put("rightIds", rightIds);
// 将refresh_token缓存到redis
String refreshToken=result.getString("refresh_token");
if (StringUtils.isNotBlank(refreshToken)&&StringUtils.isNotBlank(token)){
String redisKey="data:center:auth:logout:";
stringRedisTemplate.opsForValue().set(redisKey+token,refreshToken,1, TimeUnit.DAYS);
}
// 记录登录日志
MySchema mySchema = new MySchema();
mySchema.setSchemaname(user.getSchemaname());
mySchema.setTenantid(user.getTenantid());
MySchemaHolder.setCurrentMySchema(mySchema);
sysLogService.addSafetyLog(user, request);
return new CallBackMessage(res);
} catch (Exception e) {
log.error(e.getMessage());
log.info("sendRedirect4:{}",Configuration.getLoginUrl());
response.sendRedirect(Configuration.getLoginUrl());
}
} else {
log.info("sendRedirect4:{}",Configuration.getLoginUrl());
response.sendRedirect(Configuration.getLoginUrl());
}
return new CallBackMessage("失败");
}