什么是单点登录
单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录。
实现机制
当用户第一次访问应用系统的时候,因为还没有登录,会被引导到SSO的认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--token;用户再访问别的应用的时候,就会将这个token带上,作为自己认证的凭据,应用系统接受到请求之后会把token送到认证系统进行校验,检查toke的合法性。如果通过校验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。
实现流程
代码实现
sso子系统登录拦截器
当子系统进行操作是(比如需要订单系统提交订单时)时必须要求用户登录,可以使用拦截器来实现。拦截器的处理流程如下:
1、拦截请求url
2、从cookie中取token
3、如果没有toke跳转到登录页面。
4、取到token,需要调用sso系统的服务查询用户信息。
5、如果用户session已经过期,跳转到登录页面
6、如果没有过期,放行。
拦截器配置
<!-- 配置拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<!-- <mvc:mapping path="/order/**"/> -->
<mvc:mapping path="/item/**"/>
<bean class="com.taotao.portal.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
拦截器实现
/**
* 用户登录拦截器
* @ClassName: LoginInterceptor
* @Description: TODO(这里用一句话描述这个类的作用)
* @author Lenovo
* @date 2018年8月30日
*
*/
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Value("${SSO_LOGIN_URL}")
private String SSO_LOGIN_URL;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 1、拦截请求url
// 2、从cookie中取token
String token = CookieUtils.getCookieValue(request, "TT_TOKEN");
// 3、如果没有toke跳转到登录页面。
if(StringUtils.isEmpty(token)){
return null;
}
// 4、取到token,需要调用sso系统的服务查询用户信息。
String json = HttpClientUtil.doGet(SSO_BASE_URL + SSO_USER_TOKEN_SERVICE + token);
//吧json转换成java对象
TaotaoResult result = TaotaoResult.format(json);
if(result.getStatus() != 200){
return null;
}
//取用户对象
TaotaoResult.formatToPojo(json, TbUser.class);
TbUser user = (TbUser) result.getData();
// 5、如果用户session已经过期,跳转到登录页面
if (user == null) {
//跳转至sso认证中心;登录页url+回调参数:比如在付款页付款是调到登录也,登录后再根据回调函数,调到付款页
response.sendRedirect(SSO_LOGIN_URL + "?redirectURL=" + request.getRequestURI());
return false;
}
// 6、如果没有过期,放行。
return true;
}
//执行handler之后,但是再返回model和view之前
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object arhandlerg2, ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
注意:
1、在第三步“3、如果没有toke跳转到登录页面。”处,会跳转到接下来要分享的用户登录,如果存在token,会进到接下来要分享的通过token查询用户信息进行验证。
2、在第五步调到登录页面时,需要附带上拦截器拦截的url,以便登录成功后,直接返回之前的界面。
用户登录
用户登录protal系统时,跳转到用户登录页面,sso验证中心接受用户名和密码,并检验,如果密码正确,生成用户认证凭据-token,并把用户信息写入redis保存在sso服务端,然后把登录成功,并把token返回给客户端,并写入cookie。
@Autowired
private TbUserMapper userMapper;
@Autowired
private JedisClient jedisClient;
@Value("${REDIS_SESSION_KEY}")
private String REDIS_SESSION_KEY;
@Value("${SESSION_EXPIRE}")
private Integer SESSION_EXPIRE;
/**
* 用户登录
*/
@Override
public TaotaoResult login(String username, String password,
HttpServletRequest request, HttpServletResponse response) {
// 校验用户名和密码是否正确
TbUserExample example = new TbUserExample();
Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(username);
List<TbUser> list = userMapper.selectByExample(example);
//取用户信息
if(list == null || list.isEmpty()){
return TaotaoResult.build(400, "用户名或密码错误");
}
TbUser user = list.get(0);
//检验密码
if(user.getPassword().equals(DigestUtils.md5DigestAsHex(password.getBytes()))){
return TaotaoResult.build(400, "用户名或密码错误!");
}
//登录成功
//生成token,token即为授权令牌
String token = UUID.randomUUID().toString();
//吧用户信息写入redis
user.setPassword(null);
jedisClient.set(REDIS_SESSION_KEY + ":" + token, JsonUtils.objectToJson(user));
//设置session的过期时间
jedisClient.expire(REDIS_SESSION_KEY + ":" + token, SESSION_EXPIRE);
//写cookie,浏览器关闭,cookie失效
CookieUtils.setCookie(request, response, "TT_TOKEN", token);
return TaotaoResult.ok(token);
}
将token存储在redis中,是因为redis可以为key设置有效时间也就是令牌的有效期。redis运行在内存中,速度非常快,正好sso-server不需要持久化任何数据。
通过token查询用户信息
订单服务需要查询订单时,先从cookie中取token,并调用sso认证中心根据token到reids查询用户信息,如果用户信息不存在说明session已经过期,返回400并提示用户session已经过期,接着会跳转到sso的登录界面。如果查询到用户,返回用户信息,并且更新一下用户的过期时间。
@Autowired
private JedisClient jedisClient;
@Value("${REDIS_SESSION_KEY}")
private String REDIS_SESSION_KEY;
@Value("${SESSION_EXPIRE}")
private Integer SESSION_EXPIRE;
@Override
public TaotaoResult getUserByToken(String token) {
// 根据token取用户信息
String json = jedisClient.get(REDIS_SESSION_KEY + ":" + token);
//判断是否查询到结果
if (StringUtils.isEmpty(json)) {
return TaotaoResult.build(400, "用户session已经过期");
}
//把json转换成java对象
TbUser user = JsonUtils.jsonToPojo(json, TbUser.class);
//更新session的过期时间
jedisClient.expire(REDIS_SESSION_KEY + ":" + token, SESSION_EXPIRE);
return TaotaoResult.ok(user);
}
核心步骤
1、用户未登录时访问子站一,子站一服务器通过拦截器检测到用户没登录(没有本站session,因为没传过来session对应cookie),于是通知浏览器跳转到登录界面,并在跳转的URL参数中带上当前页面地址,以便登录后自动跳转回本页。
2、显示登录界面后,用户提交登录请求到服务端,服务端验证通过,创建和账号对应的用户登录凭据(token),并存储在key-value数据库(比如redis)。
然后,服务端通知浏览器把该token作为SSO服务站点的cookie存储起来,并跳转回子站一,跳回子站一的URL参数中会带上这个token。
/**
* 展示登录页面
*/
@RequestMapping("/page/login")
public String showLogin(String redirectURL,Model model) {
//需要把参数传递给jsp
model.addAttribute("redirect", redirectURL);
return "login";
}
3、跳转回子站一后,子站一服务端检测到浏览器请求的URL中带了回调参数,就会跳转到相应页面。
4、用户再访问子站二。子站二服务器检测到用户没登录,于是通知浏览器跳转到SSO服务站点。
5、浏览器访问SSO服务站点时会带上上述2环节创建的token这个cookie。SSO服务站点根据该token能找到对应用户,于是通知浏览器跳转回子站二,并在跳转回去的URL参数中带上这个token。
6、子站二服务端检测到浏览器请求的URL中带上了回调参数,于是又会走上述3对应步骤,跳入登录前请求的页面。