单点登录介绍
SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。
为什么要单点登录
在平常写案例的时候,如果只有一个web工程。如果要访问用户相关信息,那么我们通常会写个拦截器,从session中看能不能取到用户信息,如果不能那么需要返回到登录页面,用户登录后将用户信息保存到session中,那么在此访问用户中心就没问题了。然后这种做法在一个web工程中是没问题的。
如果系统是分布式的情况呢,比如拿商城来说,它是一个分布式的,什么会员模块,商品管理模块,购物车模块,订单模块等。如果还用上面那张验证方式的话,访问a模块的话发现没登录,然后跳转页面登录了,信息存入session中,如果下次访问的是b模块,由于模块都是存在于不同的服务器session中肯定没有登录用户信息,那么肯定是访问不通过要求重新登录。而且,为了解决高并发还得进行集群,即使是两次访问同一模块,也有可能访问的是集群中的另外一台服务器,这样就存在多次要求登录的问题。
解决:我们可以整个Session服务器(SSO系统)专门用来处理登录问题,这样用户每次访问用户中心的时候都来该服务器判断用户有没有登录,如果登录了放行,没有登录就跳转到登录页面,登录后将用户信息保存到Session服务器中,我们需要用redis来模拟从前的session,以前用户信息存入redis中,这里我们就将用户信息存入redis中。
那么怎么存,用什么做key什么做value呢?在一个web工程的时候,用户信息存入session是这样设置的session.setArrtribute(“admin”,user),获取则是session.getAttribute(“admin”);
很自然的想到用用户id做为key,但是不要这样做,因为这样是不能区分不同的连接的,比如你在A电脑登录了tom这个账号,信息存入redis中了且key为tom的id。然后换个电脑B,但是不直接登录却直接访问用户中心(比如订单结算),那么能不能访问呢?是可以访问的,因为redis中存了这么个键值对啊,然后就能取出来用户信息说明已经登录了,所以肯定给访问。但是这样是不合理的,不应该换电脑登录还能直接访问用户中心。
用一个web工程使用tomcat服务器的时候,tomcat是怎么区分不同连接的呢,实际上每次获取session的时候tomcat会生成一个JSESSIONID的作为标识,然后返回给浏览器存到Cookie里面,下次再访问的时候会带着这个JSESSIONID来访问,然后tomcat拿到这个标识会去寻找对应的session,再从中取出用户信息,这样如果换电脑B了,B浏览器里面是没有这个JSESSIONID的,那么在服务端也找不到对应的session更别说取到用户信息了,所以要求重新登录。
既然是打算用redis来模拟session,那么也可以这样做。用户每次登录的时候都会生成一个唯一的表示token,用它来作为key,用户信息作为value,然后将token存到Cookie里面返给浏览器。用户下次
访问用户中心的时候,从Cookie里面取token,再用token从redis中取用户信息,来判断是否允许访问用户中心。
这样做,只要用户换电脑登录了,那么Cookie里面就没有这个token就查不到用户信息所以必须要重新登录了,分析到这基本上也能做了。
前期准备好的jar包
e3-common(jar)里面存放的是一些工具类
e3-manager-pojo(jar)里面存放的是mybatis逆向工程生成的Mapper和映射文件。
1、SSO工程搭建
e3-sso(pom)
|–e3-sso-interface(jar)
|–e3-sso-service(war)
e3-sso-web(war)
e3-sso需要引入的jar包
pom.xml中
<groupId>cn.e3mall</groupId>
<artifactId>e3-sso</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>e3-sso-interface</module>
<module>e3-sso-service</module>
</modules>
<dependencies>
<dependency>
<groupId>cn.e3mall</groupId>
<artifactId>e3-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<!-- 配置tomcat插件 -->
<build>
<plugins>
<plugin>
<!-- 配置Tomcat插件 -->
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<path>/</path><!-- 表示访问时候不带工程名的 -->
<port>8087</port>
</configuration>
</plugin>
</plugins>
</build>
e3-sso-interface引入jar包
pom.xml中
<parent>
<groupId>cn.e3mall</groupId>
<artifactId>e3-sso</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>e3-sso-interface</artifactId>
<dependencies>
<dependency>
<groupId>cn.e3mall</groupId>
<artifactId>e3-manager-pojo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
e3-sso-service引入jar包
pom.xml中
<parent>
<groupId>cn.e3mall</groupId>
<artifactId>e3-sso</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>e3-sso-service</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>cn.e3mall</groupId>
<artifactId>e3-manager-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.e3mall</groupId>
<artifactId>e3-sso-interface</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-jms</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- dubbo相关 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.netty</groupId>
<artifactId>netty</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
</dependencies>
spring配置文件1中:配置数据源以及spring与mybatis整合
<!-- 数据库连接池 -->
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:conf/*.properties" />
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="driverClassName" value="${jdbc.driver}" />
<property name="maxActive" value="10" />
<property name="minIdle" value="5" />
</bean>
<!-- 让spring管理sqlsessionfactory 使用mybatis和spring整合包中的 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 数据库连接池 -->
<property name="dataSource" ref="dataSource" />
<!-- 加载mybatis的全局配置文件 -->
<property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.e3mall.mapper" />
</bean>
spring配置文件2中:配置事务
<!-- 事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 传播行为 -->
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="create*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
<tx:method name="select*" propagation="SUPPORTS" read-only="true" />
<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 切面 -->
<aop:config>
<aop:advisor advice-ref="txAdvice"
pointcut="execution(* cn.e3mall.sso.service..*.*(..))" />
</aop:config>
spring配置文件3中配置redis
<!-- 连接redis单机版 -->
<bean id="jedisClientPool" class="cn.e3mall.common.jedis.JedisClientPool">
<property name="jedisPool" ref="jedisPool"></property>
</bean>
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<!-- 一定要用name,构造方法太多用index容易错 -->
<constructor-arg name="host" value="192.168.25.128"/>
<constructor-arg name="port" value="6379"/>
</bean>
spring配置文件中配置:发布服务,组件扫描,写完服务再给出
web.xml中
<!-- 加载spring容器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
配置文件db.properties中配置数据库连接
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/e3mall_32?characterEncoding=utf-8
jdbc.username=root
jdbc.password=123456
resource.properties中设置token过期时间
SESSION_EXPIRE=1800
e3-sso-web引入jar包
pom.xml中
<groupId>cn.e3mall</groupId>
<artifactId>e3-sso-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>cn.e3mall</groupId>
<artifactId>e3-sso-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- JSP相关 -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</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>
<!-- 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-jms</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- dubbo相关 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.netty</groupId>
<artifactId>netty</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
</dependencies>
<!-- 配置tomcat插件 -->
<build>
<plugins>
<plugin>
<!-- 配置Tomcat插件 -->
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<path>/</path><!-- 表示访问时候不带工程名的 -->
<port>8088</port>
</configuration>
</plugin>
</plugins>
</build>
spring配置文件中除了组件扫描之外还要引用服务,但是Service层还没发布服务,所以待会给出,另外还需要将jsp页面跟静态文件都引入到项目的WEB-INF和webaap下。
web.xml中
<!-- 解决post乱码 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- springmvc的前端控制器 -->
<servlet>
<servlet-name>e3-manager</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- contextConfigLocation不是必须的, 如果不配置contextConfigLocation, springmvc的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>e3-manager</servlet-name>
<!-- 伪静态化,搜索引擎优化,搜索引擎会先找静态页面 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
属性配置文件resource.properties中
TOKEN_KEY=token
功能实现
服务层:e3-sso-service中
- 接收参数,判断用户名和密码正确
- 用户名和密码都正确的话,生成token,相当于tomcat时候的jsessionId.用uuid生成,保证唯一性。
- 将用户信息存入redis。Key为”SESSION”+token,value为查询的用户信息转为的json串。
- 设置key的有效期。一般为半个小时
- 返回包装了token的E3Result
/*
* 用户登录处理
*/
@Service
public class LoginServiceImpl implements LoginService{
@Autowired
private TbUserMapper userMapper;
@Autowired
private JedisClient jedisClient;
@Value("${SESSION_EXPIRE}")
private Integer SESSION_EXPIRE;
public E3Result userLogin(String username, String password) {
//1.判断用户名和密码是否正确
//根据用户名查询用户信息
TbUserExample example = new TbUserExample();
Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(username);
List<TbUser> list = userMapper.selectByExample(example);
if(list == null || list.size() == 0){
//返回登录失败
return E3Result.build(400, "用户名或者密码错误");
}
//取用户信息
TbUser user = list.get(0);
//判断密码是否正确
if(!DigestUtils.md5DigestAsHex(password.getBytes()).equals(user.getPassword())){
//2.如果不正确返回登录失败
return E3Result.build(400, "用户名或者密码错误");
}
//3.如果正确生成token
String token = UUID.randomUUID().toString();//生成的uuid必不重复
//4.把用户信息写入redis,key:token,value:用户信息
user.setPassword(null);
jedisClient.set("SESSION:"+ token, JsonUtils.objectToJson(user));
//5.设置Session的过期时间
jedisClient.expire("SESSION:"+ token, SESSION_EXPIRE);
//6.把token返回
return E3Result.ok(token);
}
}
注:如果没用过mybatis逆向工程那么花2小时学一下这样看上面的代码更清晰。
E3Result为自定义响应类,如下
public class E3Result implements Serializable{
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
// 响应业务状态
private Integer status;
// 响应消息
private String msg;
// 响应中的数据
private Object data;
public static E3Result build(Integer status, String msg, Object data) {
return new E3Result(status, msg, data);
}
public static E3Result ok(Object data) {
return new E3Result(data);
}
public static E3Result ok() {
return new E3Result(null);
}
public E3Result() {
}
public static E3Result build(Integer status, String msg) {
return new E3Result(status, msg, null);
}
public E3Result(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public E3Result(Object data) {
this.status = 200;
this.msg = "OK";
this.data = data;
}
get、set方法
}
服务写完之后需要发布服务(使用了Dubbo发布服务,Zookeeper作为注册中心)
配置文件4中:组件扫描,发布服务
<context:component-scan base-package="cn.e3mall.sso.service"/>
<!-- 使用dubbo发布服务 -->
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="e3-sso" />
<dubbo:registry protocol="zookeeper"
address="192.168.25.128:2181" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20883" /><!-- 一个服务对应一个端口 -->
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="cn.e3mall.sso.service.LoginService"
ref="loginServiceImpl" timeout="600000"/>
<dubbo:service>
然后将e3-sso工程安装到本地仓库
表现层:e3-sso-web中
先要接收服务,spring配置文件中
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:conf/resource.properties" />
<context:component-scan base-package="cn.e3mall.sso.controller" />
<mvc:annotation-driven />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<mvc:resources location="/css/" mapping="/css/**"/>
<mvc:resources location="/js/" mapping="/js/**"/>
<mvc:resources location="/images/" mapping="/images/**"/>
<!-- 引用dubbo服务 -->
<dubbo:application name="e3-sso-web"/>
<dubbo:registry protocol="zookeeper" address="192.168.25.128:2181"/>
<dubbo:reference interface="cn.e3mall.sso.service.LoginService" id="loginService" />
处理流程:
1. 传入用户名和密码,调用service层。获取E3Result。
2. 根据status判断登录是否成功
3. 从获得的E3Result中取token信息,存入cookie
4. 返回结果给页面
/*
* 用户登录处理
*/
@Controller
public class LoginController {
@Autowired
private LoginService loginService;
@Value("${TOKEN_KEY}")
private String TOKEN_KEY;//TOKEN_KEY=token
@RequestMapping("/page/login")
public String toLogin(String redirect,Model model){
model.addAttribute("redirect", redirect);
//返回登录页面
return "login";
}
@RequestMapping(value="/user/login",method=RequestMethod.POST)
@ResponseBody
public E3Result login(String username,String password,
HttpServletRequest request,HttpServletResponse response){
E3Result result = loginService.userLogin(username, password);
//判断是否登录成功
if(result.getStatus()==200){
String token = (String) result.getData();
//如果登录成功,token写入cookie
CookieUtils.setCookie(request, response, TOKEN_KEY, token);
}
return result;
}
}
注:cookie一般默认是不能跨域的(商城采用了分布式架构,所以每个模块对应的域名肯定是不一样的,将比如上面的e3-sso端口号是8089,e3-sso-web是8090),但是这里写的Cookie工具类CookieUtils的setCookie()方法中对跨域是进行了处理的。
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
到这里工程就算做完了。
当用户登录的时候,先进行校验,校验通过后,生成token,作为key,查出来的用户信息作为value存入到redis中并设置key的过期时间。并且将token返回给表现层,表现层将token存入Cookie中。当用户访问其它模块,比如订单模块的时候,我们可以写个拦截器,拦截请求,判断用户是否登录,从Cookie中取token,如果没取到token说明用户根本没登录所以跳转到登录页面,如果取到了token,那么根据token去redis中查询用户信息,说明key已经失效了,跳转到登录页面。否则放行。
比如访问购物车系统(天猫访问购物车要求是登录状态下):
在e3-order-web工程中编写拦截器
/*
* 用户登录处理
*/
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//前处理,执行handler之前执行此方法
//返回true:放行 false:拦截
//1.从cookie中取token
String token = CookieUtils.getCookieValue(request, "token");
//2.如果没有token,未登录状态
if(StringUtils.isBlank(token)){
return true;
}
//3.如果取到token,需要调用sso系统的服务,根据token取用户信息
E3Result e3Result = tokenService.getUserByToken(token);
if (e3Result.getStatus()!=200){
//4.没有取到用户信息,登录已经过期,直接放行
return true;
}
//5.取到用户信息。登录状态。
TbUser user = (TbUser) e3Result.getData();
//6.把用户信息放到request中,只需要在controller中判断request中是否包含user信息。
request.setAttribute("user", user);
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
//handler执行之后,返回modelAndView之前
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
//完成处理,返回modelAndView之后(已经响应了)
//可以再次处理异常
}
}
注:这里需要在sso系统中发布一个新的服务
<dubbo:service interface="cn.e3mall.sso.service.TokenService"
ref="tokenServiceImpl" timeout="600000"/>
该服务是根据token取用户信息
/*
* 根据token取用户信息
*/
@Service
public class TokenServiceImpl implements TokenService{
@Autowired
private JedisClient jedisClient;
@Value("${SESSION_EXPIRE}")
private Integer SESSION_EXPIRE;
public E3Result getUserByToken(String token) {
//根据token到redis中取用户信息
String json = jedisClient.get("SESSION:"+ token);
if(StringUtils.isBlank(json)){
//取不到信息,登录过期,返回登录过期
return E3Result.build(201, "用户登录已经过期");
}
//取到用户信息,跟新token的过期时间
jedisClient.expire("SESSION:"+ token, SESSION_EXPIRE);
//返回结果,E3Result其中包含用户对象
TbUser user = JsonUtils.jsonToPojo(json, TbUser.class);
return E3Result.ok(user);
}
}
然后在e3-cart-web工程中引用该服务
<dubbo:reference interface="cn.e3mall.sso.service.TokenService" id="tokenService" />
测试:
点击登录。提示登录成功后会跳转页面到首页.
查看redis