8.单点登录系统
[taotao]-单点登录系统
1.SSO介绍
【1】基本概念
什么是单点登录系统?
SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一
传统登录模式
在并发量高的情况下,一个tomcat无法支撑业务需求,需要通过集群方式部署项目,但基于这种方式下需要考虑session共享的问题:tomcat做集群配置session复制。如果集群中节点很多,会形成网络风暴,推荐节点数量不要超过5个。
分布式架构
采用分布式架构,将系统实现拆分成多个子系统
系统架构
【2】SSO工程创建
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.taotao</groupId>
<artifactId>taotao-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.taotao</groupId>
<artifactId>taotao-sso</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>com.taotao</groupId>
<artifactId>taotao-manager-mapper</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Redis客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
<!-- 添加tomcat插件 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8084</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
框架整合
可参考taotao-rest工程构建,需要修改的内容说明如下:
2.SSO实现
taotao-sso主要为其他子系统提供注册、登录、查询功能,其发布类似于taotao-rest服务概念,只不过将具体的功能定位到集中登录这点,所有的子系统通过该模块验证注册、登录等信息,从而达到统一登录的目的
【1】数据校验接口
功能分析
要查询的表tb_user。根据查询条件到数据库中进行查询,如果查询到结果返回false,查询结果为空返回true。使用taotaoResult包装一下返回。需要支持jsonp,根据callback判断
dao层
可以使用逆向工程
service层
接收两个参数,一个是要校验的数据,一个是数据类型。根据不同的数据类型生成不同的查询条件,到tb_user中进行查询如果查询到结果返回false,查询结果为空返回true。
参数:String param、Int type
返回值:TaotaoResult
@Service
public class RegisterServiceImpl implements RegisterService {
@Autowired
private TbUserMapper userMapper;
@Override
public TaotaoResult checkData(String param, int type) {
//根据数据类型检查数据
TbUserExample example = new TbUserExample();
Criteria criteria = example.createCriteria();
//1、2、3分别代表username、phone、email
if (1 == type) {
criteria.andUsernameEqualTo(param);
} else if ( 2 == type) {
criteria.andPhoneEqualTo(param);
} else if ( 3 == type ) {
criteria.andEmailEqualTo(param);
}
//执行查询
List<TbUser> list = userMapper.selectByExample(example);
//判断查询结果是否为空
if (list == null || list.isEmpty()) {
return TaotaoResult.ok(true);
}
return TaotaoResult.ok(false);
}
}
controller层
Controller发布服务:接收三个参数,分别为校验的数据、数据类型、callback。调用Service校验。返回json数据。需要支持jsonp,需要判断callback
@Controller
@RequestMapping("/user")
public class RegisterController {
@Autowired
private RegisterService registerService;
@RequestMapping("/check/{param}/{type}")
@ResponseBody
public Object checkData(@PathVariable String param, @PathVariable Integer type, String callback) {
try {
TaotaoResult result = registerService.checkData(param, type);
if (StringUtils.isNotBlank(callback)) {
//请求为jsonp调用,需要支持
MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result);
mappingJacksonValue.setJsonpFunction(callback);
return mappingJacksonValue;
}
return result;
} catch (Exception e) {
e.printStackTrace();
return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
}
}
}
测试
测试:启动taotao-sso进行测试
通过不同的数据测试数据校验接口,对多个字段和类型进行匹配测试
【2】注册接口
功能分析
使用的表tb_user,向tb_user表中插入数据。插入之前还需要进行数据校验,如果校验失败返回state为400,如果校验成功执行插入操作,完成后返回TaotaoResult.ok
dao层
可以使用逆向工程
service层
参数:TbUser
返回值:TaotaoResult
接收参数,对数据进行校验,校验成功,插入数据,返回结果
// 接口定义
public TaotaoResult register(TbUser user);
// 接口实现
@Override
public TaotaoResult register(TbUser user) {
// 校验数据
//校验用户名、密码不能为空
if (StringUtils.isBlank(user.getUsername())
|| StringUtils.isBlank(user.getPassword())) {
return TaotaoResult.build(400, "用户名或密码不能为空");
}
//校验数据是否重复
//校验用户名
TaotaoResult result = checkData(user.getUsername(), 1);
if (!(boolean) result.getData()) {
return TaotaoResult.build(400, "用户名重复");
}
//校验手机号
if (user.getPhone() != null) {
result = checkData(user.getPhone(), 2);
if (!(boolean) result.getData()) {
return TaotaoResult.build(400, "手机号重复");
}
}
//校验手机号
if (user.getEmail() != null) {
result = checkData(user.getEmail(), 3);
if (!(boolean) result.getData()) {
return TaotaoResult.build(400, "邮箱重复");
}
}
//插入数据
user.setCreated(new Date());
user.setUpdated(new Date());
// 进行MD5密码加密
user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()));
userMapper.insert(user);
return TaotaoResult.ok();
}
controller层
接收一个表单,请求的方法为post。使用TbUser接收表单的内容。调用Service插入数据,返回
@RequestMapping(value="/register", method=RequestMethod.POST)
@ResponseBody
public TaotaoResult register(TbUser user) {
try {
TaotaoResult result = registerService.register(user);
return result;
} catch (Exception e) {
e.printStackTrace();
return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
}
}
账号密码加密:spring提供了MD5加密(org\springframework\spring-core\4.1.3.RELEASE\spring-core-4.1.3.RELEASE.jar\DigestUtil),也可借助自定义工具类
测试
测试接口:访问路径:http://localhost:8084/sso/user/register,重复访问测试会报相应的错误(重复名称验证等)
【3】用户登录接口
功能分析
接收用户名和密码
校验用户名密码
生成token,可以使用UUID
把用户信息写入redis,key就是token
把token写入cookie
返回登录成功需要把token返回给客户端
CookieUtils工具类
借助CookieUtils进行操作,在taotao-common下引入CookieUtils工具类(工具类中引用了servt-api.jar相关的内容),右键工程,选择Properties,搜索“Java Build Path”,在Libraries选项卡中选择“Add Library”添加Server Runtime,选择外部安装的tomcat环境,添加完成选择应用,刷新工程(解决本地编译问题)
添加完成之后需要重新将taotao-common导入到本地maven仓库,虽然能够正常导入相关包,但实际上打包的时候并没有定义因此无法将相关包引入,此处需要将所需的jar加载到taotao-common工程中,即通过pom.xml添加依赖,随后再进行打包测试
dao层
使用逆向工程生成的代码
service层
接收参数:用户名、密码。校验密码是否正确,生成token,向redis中写入用户信息,把token写入cookie,返回TaotaoResult包含token。
参数:用户名、密码、HttpServletResponse、HttpServletRequest
返回值:TaotaoResult
-- resource.properties
# SESSION EXPIRE
SESSION_EXPIRE=1800
// 接口定义
public TaotaoResult login(String username, String password, HttpServletRequest request,
HttpServletResponse response)
// 接口实现
package com.taotao.sso.service.impl;
@Service
public class LoginServiceImpl implements LoginService {
@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
String token = UUID.randomUUID().toString();
//把用户信息写入redis
//key:REDIS_SESSION:{TOKEN}
//value:user转json
user.setPassword(null);
jedisClient.set(REDIS_SESSION_KEY + ":" + token, JsonUtils.objectToJson(user));
//设置session的过期时间
jedisClient.expire(REDIS_SESSION_KEY + ":" + token, SESSION_EXPIRE);
//写cookie
CookieUtils.setCookie(request, response, "TT_TOKEN", token);
return TaotaoResult.ok(token);
}
}
controller层
调用Service,返回taotaoResult对象,响应json数据
请求url:/user/login
接收参数:username、password
package com.taotao.sso.controller;
@Controller
public class LoginController {
@Autowired
private LoginService loginService;
@RequestMapping(value="/user/login", method=RequestMethod.POST)
@ResponseBody
public TaotaoResult login(String username, String password,
HttpServletRequest request, HttpServletResponse response) {
try {
TaotaoResult result = loginService.login(username, password, request, response);
return result;
} catch (Exception e) {
e.printStackTrace();
return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
}
}
}
测试
【4】根据token查询用户信息
功能分析
根据token到redis查询用户信息,如果用户信息不存在说明session已经过期,返回400并提示用户session已经过期。如果查询到用户,返回用户信息,并且更新一下用户的过期时间。
请求url:/user/token/
返回:TaotaoResult(需要支持jsonp)
dao层
使用JedisClient实现
service层
根据token查询redis,查询到结果返回用户对象,更新过期时间。如果查询不到结果,返回Session已经过期,状态码400.
参数:String token
返回值:TaotaoResult
// 接口定义
public TaotaoResult getUserByToken(String token);
// 接口实现
@Override
public TaotaoResult getUserByToken(String token) {
// 根据token取用户信息
String json = jedisClient.get(REDIS_SESSION_KEY + ":" + token);
//判断是否查询到结果
if (StringUtils.isBlank(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);
}
controller层
从url中取token的内容,调用Service取用户信息,返回TaotaoResult。(json数据)
- 请求url:/user/token/
@RequestMapping("/user/token/{token}")
@ResponseBody
public Object getUserByToken(@PathVariable String token, String callback) {
try {
TaotaoResult result = loginService.getUserByToken(token);
//支持jsonp调用
if (StringUtils.isNotBlank(callback)) {
MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result);
mappingJacksonValue.setJsonpFunction(callback);
return mappingJacksonValue;
}
return result;
} catch (Exception e) {
e.printStackTrace();
return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
}
}
测试
测试:先随便登录一个账号信息(http://localhost:8084/sso/user/login),随后通过http://localhost:8084/sso/user/token/a372312f-ac8f-490e-8b7f-442f6ca7f58d访问测试
【5】安全退出
功能分析
登录注销功能
dao层
使用逆向工程生成
service层
// 接口定义
public TaotaoReult logout(String token);
controller层
@RequestMapping("/user/logout/{token}")
@ResponseBody
public Object logout(@PathVariable String token, String callback) {
try {
TaotaoResult result = loginService.logout(token);
// 支持jsonp调用
if (StringUtils.isNotBlank(callback)) {
MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result);
mappingJacksonValue.setJsonpFunction(callback);
return mappingJacksonValue;
}
return result;
} catch (Exception e) {
e.printStackTrace();
return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
}
}
测试
先登录后获取登录token,随后注销再查看token信息
3.SSO整合业务系统
【1】登录注册功能页面嵌入
资源配置
把静态页面添加到sso系统
Springmvc设置拦截,需要配置资源映射
登录、注册页面展示(controller配置出口)
package com.taotao.sso.controller;
@Controller
public class PageController {
/**
* 展示登录页面
*/
@RequestMapping("/page/login")
public String showLogin() {
return "login";
}
/**
* 展示注册页面
*/
@RequestMapping("/page/register")
public String showRegister() {
return "register";
}
}
访问链接,如果出现页面访问css样式失效的问题,则需要查看是否拦截配置的问题导致出错:此处设置taotao-sso配置拦截url的请求,并在springmvc.xml中配置静态资源映射,访问测试,数据正常显示
登录、注册逻辑说明
用户页面输入注册信息,要对数据的有效性进行检查(调用sso系统的接口判断数据的有效性)
提交表单,提交到用户注册接口
【2】门户系统整合SSO
首页展示用户
启动taotao-rest、taotao-portal、taotao-sso测试登录、注册,查看存储的数据信息
登录拦截器
当用户提交订单时此时必须要求用户登录,可以使用拦截器来实现。拦截器的处理流程说明如下:
拦截请求url,从cookie中取token
如果没有token跳转到登录页面
取到token,需要调用sso系统的服务查询用户信息
- 如果用户session已经过期,跳转到登录页面
- 如果没有过期,放行
service层
根据token取用户信息,如果取到返回TbUser对象,如果取不到,返回null
参数:Request、Response
返回值:TbUser
SSO_BASE_URL=http://localhost:8084
SSO_BASE_URL=/user/token/
// 接口定义
public TbUser getUserByToken(HttpServletRequest request, HttpServletResponse response);
// 接口实现
package com.taotao.portal.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Value("${SSO_BASE_URL}")
private String SSO_BASE_URL;
@Value("${SSO_USER_TOKEN_SERVICE}")
private String SSO_USER_TOKEN_SERVICE;
@Override
public TbUser getUserByToken(HttpServletRequest request, HttpServletResponse response) {
try {
//从cookie中取token
String token = CookieUtils.getCookieValue(request, "TT_TOKEN");
//判断token是否有值
if (StringUtils.isBlank(token)) {
return null;
}
//调用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;
}
//取用户对象
result = TaotaoResult.formatToPojo(json, TbUser.class);
TbUser user = (TbUser) result.getData();
return user;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
拦截器实现
在springmvc中需要实现HandlerInterceptor接口
SSO_LOGIN_URL=http://localhost:8084/page/login
package com.taotao.portal.interceptor;
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
// 3、如果没有toke跳转到登录页面。
// 4、取到token,需要调用sso系统的服务查询用户信息。
TbUser user = userService.getUserByToken(request, response);
// 5、如果用户session已经过期,跳转到登录页面
if (user == null) {
response.sendRedirect(SSO_LOGIN_URL);
return false;
}
// 6、如果没有过期,放行。
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
Springmvc中配置拦截器
处理登录回调
登录的url中包含回调的参数,参数就是回调的url,sso登录Controller接收参数,当登录成功后跳转到回调的url
controller层
package com.taotao.sso.controller;
@Controller
public class PageController {
/**
* 展示登录页面
*/
@RequestMapping("/page/login")
public String showLogin(String redirectURL,Model model) {
// 将参数传递给jsp
model.addAttribute("redirect",redirectURL);
return "login";
}
}
jsp逻辑处理说明
拦截器设置
首次未登录直接访问商品详情页面被拦截,需要强制登录才能操作,登录完成之后返回被拦截的url