Fork me on GitHub

十一.宜立方商城——SSO单点登录

一.课程计划

1
2
3
4
1、sso注册功能实现
2、sso登录功能实现
3、通过token获得用户信息
4、Ajax跨域请求(jsonp)

二.Sso系统工程搭建

image

1
2
3
4
5
需要创建一个sso服务工程,可以参考e3-manager创建。
e3-sso(pom聚合工程)
|--e3-sso-interface(jar)
|--e3-sso-Service(war)
e3-sso-web

image

image

image

image

image

image

image

image

image

image

三.服务接口实现


功能分析
  1. 先看数据库中的User表

image

  1. 再看注册页面

image

image

  1. 页面请求详解

image

image

image

image

image

image

image

image

3.1 检查数据是否可用

3.1.1 功能分析

请求的url:/user/check/{param}/{type}

参数:从url中取参数1、String param(要校验的数据)2、Integer type(校验的数据类型)

响应的数据:json数据。e3Result,封装的数据校验的结果true:成功false:失败。

业务逻辑:

  1. 从tb_user表中查询数据
  2. 查询条件根据参数动态生成。
  3. 判断查询结果,如果查询到数据返回false。
  4. 如果没有返回true。
  5. 使用e3Result包装,并返回。

3.1.2 Dao层

从tb_user表查询。可以使用逆向工程。

3.1.3 Service

参数:

  1. 要校验的数据:String param
  2. 数据类型:int type(1、2、3分别代表username、phone、email)
  3. 返回值:e3Result

接口定义

image

实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @author RickYinPeng
* @ClassName RegisterServiceImpl
* @Description 用户注册处理
* @date 2018/12/9/12:09
*/
@Service
public class RegisterServiceImpl implements RegisterService{

@Autowired
private TbUserMapper tbUserMapper;

@Override
public E3Result checkData(String param, int type) {
//根据不同的type生成不同的查询条件
TbUserExample tbUserExample = new TbUserExample();
TbUserExample.Criteria criteria = tbUserExample.createCriteria();
//1:用户名 2:手机号 3:邮箱
if(type==1){
criteria.andUsernameEqualTo(param);
}else if(type==2){
criteria.andPhoneEqualTo(param);
}else if(type==3){
criteria.andEmailEqualTo(param);
}else {
return E3Result.build(400,"数据类型错误");
}
//执行查询
List<TbUser> tbUserList = tbUserMapper.selectByExample(tbUserExample);

//判断结果中是否包含数据
if(tbUserList!=null && tbUserList.size()>0){
//如果有数据返回false
return E3Result.ok(false);
}
//如果没有数据返回true
return E3Result.ok(true);
}
}

image

发布服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.2.xsd ">

<!-- 配置Service扫描(可以扫描到子包) -->
<context:component-scan base-package="yp.e3mall.sso.service"/>

<!-- 使用dubbo发布服务 -->
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="e3_sso" />
<dubbo:registry protocol="zookeeper"
address="192.168.25.128:2181" />
<!-- 用dubbo协议在20881端口暴露服务 -->
<!--端口会冲突,所以改为20881,之前是20880-->
<dubbo:protocol name="dubbo" port="20883" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="yp.e3mall.sso.service.RegisterService" ref="registerServiceImpl"
timeout="600000"/>
</beans>

image

3.1.4 表现层

需要在e3-sso-web中实现

image

引用服务

Controller

image

image

image

image

请求的url:/user/check/{param}/{type}

参数:从url中取参数

1、String param(要校验的数据)

2、Integer type(校验的数据类型)

响应的数据:json数据。e3Result,封装的数据校验的结果true:成功false:失败。

1
2
3
4
5
6
@ResponseBody
@RequestMapping("/user/check/{param}/{type}")
public E3Result checkData(@PathVariable String param,@PathVariable Integer type){
E3Result e3Result = registerService.checkData(param, type);
return e3Result;
}

3.2 用户注册

3.2.1 功能分析

请求的url:/user/register

参数:表单的数据:username、password、phone、email

返回值:json数据。e3Result

接收参数:使用TbUser对象接收。

请求的方法:post

业务逻辑:

1、使用TbUser接收提交的请求。

2、补全TbUser其他属性。

3、密码要进行MD5加密。

4、把用户信息插入到数据库中。

5、返回e3Result。

3.2.2 Dao层

可以使用逆向工程。

3.2.3 Service层

参数:TbUser

返回值:e3Result

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import yp.e3mall.common.utils.E3Result;
import yp.e3mall.pojo.TbUser;

public interface RegisterService {

/**
* 检查数据是否存在接口
* @param param 用户名/手机号/Email
* @param type 1:用户名 2:手机号 3:Email
* @return
*/
E3Result checkData(String param,int type);

/**
* 用户注册
* @param tbUser 需要注册的用户信息
* @return
*/
E3Result register(TbUser tbUser);

}

实现类中注册方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
public E3Result register(TbUser tbUser) {
//数据有效性校验
if(StringUtils.isBlank(tbUser.getUsername())
|| StringUtils.isBlank(tbUser.getPassword())
|| StringUtils.isBlank(tbUser.getPhone())){
return E3Result.build(400,"用户数据不完整,注册失败");
}
//1:用户名 2:手机号 3:邮箱
E3Result result = checkData(tbUser.getUsername(), 1);
if(!(boolean)result.getData()){
return E3Result.build(400,"此用户名已经被占用");
}
result = checkData(tbUser.getPhone(),2);
if(!(boolean)result.getData()){
return E3Result.build(400,"此手机号已经被占用");
}
//补全pojo的属性
tbUser.setCreated(new Date());
tbUser.setUpdated(new Date());
//对密码进行MD5加密
String md5Pass = DigestUtils.md5DigestAsHex(tbUser.getPassword().getBytes());
tbUser.setPassword(md5Pass);
//把用户数据插入到数据库中
tbUserMapper.insert(tbUser);

return E3Result.ok();
}

image

spring框架自带MD5加密技术,所以可以直接使用。

3.2.4 表现层

image

image

image

Controller:

请求的url:/user/register

参数:表单的数据:username、password、phone、email

返回值:json数据。e3Result

接收参数:使用TbUser对象接收。

请求的方法:post

1
2
3
4
5
6
@ResponseBody
@RequestMapping(value = "/user/register" ,method = RequestMethod.POST)
public E3Result register(TbUser tbUser){
E3Result result = registerService.register(tbUser);
return result;
}

3.2.5 测试

image

3.3 用户登录

image

3.3.1 功能分析

请求的url:/user/login

请求的方法:POST

参数:username、password,表单提交的数据。可以使用方法的形参接收。

返回值:json数据,使用e3Result包含一个token。

业务逻辑:

登录的业务流程:

image

登录的处理流程:

  1. 登录页面提交用户名密码。
  2. 登录成功后生成token。Token相当于原来的jsessionid,字符串,可以使用uuid。
  3. 把用户信息保存到redis。Key就是token,value就是TbUser对象转换成json。
  4. 使用String类型保存Session信息。可以使用“前缀:token”为key
  5. 设置key的过期时间。模拟Session的过期时间。一般半个小时。
  6. 把token写入cookie中。
  7. Cookie需要跨域。例如www.e3.com\sso.e3.com\order.e3.com,可以使用工具类。
  8. Cookie的有效期。关闭浏览器失效。
  9. 登录成功。

3.3.2 Dao层

查询tb_user表。单表查询。可以使用逆向工程。

3.3.3 Service层

参数:

1、用户名:String username

2、密码:String password

返回值:e3Result,包装token。

业务逻辑:

1、判断用户名密码是否正确。

2、登录成功后生成token。Token相当于原来的jsessionid,字符串,可以使用uuid。

3、把用户信息保存到redis。Key就是token,value就是TbUser对象转换成json。

4、使用String类型保存Session信息。可以使用“前缀:token”为key

5、设置key的过期时间。模拟Session的过期时间。一般半个小时。

6、返回e3Result包装token。

接口:

1
2
3
4
5
6
7
8
9
10
11
public interface LoginService {

/**
* 用户登录逻辑
* @param username 用户名
* @param password 密码
* @return
*/
E3Result userLogin(String username ,String password);

}

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* @author RickYinPeng
* @ClassName LoginServiceImpl
* @Description 用户登录处理
* @date 2018/12/16/13:27
*/
@Service
public class LoginServiceImpl implements LoginService {

@Autowired
private TbUserMapper tbUserMapper;

@Autowired
private JedisClient jedisClient;

@Value("${SESSION_EXPIRE}")
private String SESSION_EXPIRE;

@Override
/**
* 参数:用户名和密码
* 业务逻辑:
* 1:判断用户和密码是否正确
* 2:如果不正确返回登录失败
* 3:如果正确生成token
* 4:把用户信息写入redis,key:token value:用户信息
* 5:设置Session过期时间
* 6:把token返回(在服务层将token写入cookie中)
*
* 返回值:E3Result,其中包含token信息
*/
public E3Result userLogin(String username, String password) {
// 1:判断用户和密码是否正确
//根据用户名查询用户信息
TbUserExample tbUserExample = new TbUserExample();
TbUserExample.Criteria criteria = tbUserExample.createCriteria();
criteria.andUsernameEqualTo(username);
//执行查询
List<TbUser> userList = tbUserMapper.selectByExample(tbUserExample);

// 2:如果不正确返回登录失败
if(userList==null || userList.size()==0){
//返回登录失败
return E3Result.build(400,"用户名或密码错误");
}
//取用户信息
TbUser tbUser = userList.get(0);
//判断密码是否正确
if(DigestUtils.md5DigestAsHex(password.getBytes()).equals(tbUser.getPassword())){
return E3Result.build(400,"用户名或密码错误");
}

// 3:如果正确生成token
String token = UUID.randomUUID().toString();

// 4:把用户信息写入redis,key:token value:用户信息
tbUser.setPassword(null);
jedisClient.set("SESSION:"+token, JsonUtils.objectToJson(tbUser));

// 5:设置Session过期时间
jedisClient.expire("SESSION:"+token, Integer.parseInt(SESSION_EXPIRE));

// 6:把token返回(在服务层将token写入cookie中)


// 返回值:E3Result
return E3Result.ok(token);
}
}

发布服务:

1
2
<dubbo:service interface="yp.e3mall.sso.service.LoginService" ref="loginServiceImpl"
timeout="600000"/>

3.3.4 表现层

引用服务:

image

Controller

分析:
image

image

image

image

image

image

请求的url:/user/login

请求的方法:POST

参数:username、password,表单提交的数据。可以使用方法的形参接收。

HttpServletRequest、HttpServletResponse

返回值:json数据,使用e3Result包含一个token。

业务逻辑:

  1. 接收两个参数。
  2. 调用Service进行登录。
  3. 从返回结果中取token,写入cookie。Cookie要跨域。

    Cookie二级域名跨域需要设置:

    ①setDomain,设置一级域名:

     .itcatst.cn

     .e3.com

     .e3.com.cn

    ②setPath。设置为“/”

工具类放到e3-common工程中。

响应数据。Json数据。e3Result,其中包含Token。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @author RickYinPeng
* @ClassName LoginController
* @Description 用户登录处理
* @date 2018/12/16/12:44
*/
@Controller
public class LoginController {

@Autowired
private LoginService loginService;

@RequestMapping("/page/login")
public String showLogin(){
return "login";
}

@RequestMapping(value = "/user/login",method = RequestMethod.POST)
@ResponseBody
public E3Result login(String username, String password
, HttpServletRequest request, HttpServletResponse response){
E3Result e3Result = loginService.userLogin(username, password);
//判断是否登录成功
if(e3Result.getStatus()==200){
String token = e3Result.getData().toString();
//如果登录成功需要将token写入cookie中
CookieUtils.setCookie(request,response,"token",token);
}
return e3Result;
}

}

3.4 通过token查询用户信息

3.4.1 功能分析

请求的url:/user/token/{token}

参数:String token需要从url中取。

返回值:json数据。使用e3Result包装Tbuser对象。

业务逻辑:

1、从url中取参数。

2、根据token查询redis。

3、如果查询不到数据。返回用户已经过期。

4、如果查询到数据,说明用户已经登录。

5、需要重置key的过期时间。

6、把json数据转换成TbUser对象,然后使用e3Result包装并返回。

3.4.2 Dao层

使用JedisClient对象。

3.4.3 Service层

参数:String token

返回值:e3Result

接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据Token查询用户信息
*/
public interface TokenService {

/**
* 根据token获取用户信息
* @param token
* @return
*/
E3Result getUserByToken(String token);

}

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @author RickYinPeng
* @ClassName TokenServiceImpl
* @Description 根据token取用户信息
* @date 2018/12/17/16:54
*/
@Service
public class TokenServiceImpl implements TokenService{

@Autowired
private JedisClient jedisClient;

@Value("${SESSION_EXPIRE}")
private String SESSION_EXPIRE;

@Override
public E3Result getUserByToken(String token) {
//根据token到redis中获取用户信息
String json = jedisClient.get("SESSION:" + token);
//取不到用户信息,登录已经过期,返回登录过期
if(StringUtils.isBlank(json)){
return E3Result.build(400,"用户登录已经过期");
}
//取到用户信息更新token的过期时间
jedisClient.expire("SESSION:"+token, Integer.parseInt(SESSION_EXPIRE));
//返回结果,E3Result其中包含TbUser对象
TbUser tbUser = JsonUtils.jsonToPojo(json, TbUser.class);
return E3Result.ok(tbUser);
}
}

3.4.4 表现层

请求的url:/user/token/{token}

参数:String token需要从url中取。

返回值:json数据。使用e3Result包装Tbuser对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author RickYinPeng
* @ClassName TokenController
* @Description 根据token查询用户信息
* @date 2018/12/17/18:24
*/
@Controller
public class TokenController {

@Autowired
private TokenService tokenService;

@RequestMapping("/user/token/{token}")
@ResponseBody
public E3Result getUserByToken(@PathVariable String token){
E3Result result = tokenService.getUserByToken(token);
return result;
}
}

四.登录注册页面整合首页

image

4.1 首页跳转到登录、注册页面

第一步:把静态页面添加到工程中。

第二步:展示页面。

请求的url:

登录:/page/login

注册:/page/register

参数:无

返回结果:逻辑视图String

image

image

image

image

image

4.2 首页展示用户名(可以作为一个难点来说)


1、当用户登录成功后,在cookie中有token信息。

2、从cookie中取token根据token查询用户信息。

3、把用户名展示到首页。


方案一:在Controller中取cookie中的token数据,调用sso服务查询用户信息。

方案二:当页面加载完成后使用js取token的数据,使用ajax请求查询用户信息。


之前我们不是有个接口是 /user/token/{token} 这个,我们的方案是使用第二种,使用js来请求这个接口,这样就可以拿出用户信息了,然后给每个页面引入这个js就可以了。

js处理流程:

image

接下来我们先在首页写这么一个js,测试一下

分析:
image

image

image

image

image

image

image

> 这是为什么呢???

因为这个ajax请求(就请求token的)是属于 e3_portal_web(localhost:8083) 工程中的,而我们的ajax请求的是 e3_sso_service(localhost:8089) 工程中的,这就牵扯跨域问题了。
我们下面来看一下什么事跨域

Js不可以跨域请求数据。

什么是跨域:

1、域名不同

2、域名相同端口不同。





解决js的跨域问题可以使用jsonp。

Jsonp不是新技术,跨域的解决方案。使用js的特性绕过跨域请求。Js可以跨域加载js文件。

4.3.Jsonp原理

image


我们可以来分析一下这个图,在传统的Ajax跨域请求的时候,即使我们请求成功了,而且可以在浏览器中看到状态是200,但是就是没有返回的数据,这是为什么呢?可能是出于安全着想,所以浏览器是不会让你访问的。

而我们如何处理呢?

我们可以上面的图,首先我们在写跨域请求的Ajax的js文件中写这样一个js函数mycall(),先不管这是干啥的。然后在我们写ajax请求的时候加上callback这个参数,当我们后台接受到这个请求的时候就去检查它的参数中有没有callback,有就代表这是一个跨域的ajax请求,需要我们后台来配合,于是我们后台返回这样一个数据 mycall({id:1,name:z});。

这其实是利用js可以跨域加载js文件,当 mycall({id:1,name:z}); 这个数据到达浏览器后就触发浏览器中 mycall()的这个方法,从而拿到数据。

后台返回的其实就是个方法调用。

当然这样太麻烦了啊,我们还要自己写这么多的js,而且我不太擅长js,所以人家jquery给我们封装好了,我们只需要将之前我们注释的ajax请求中的dataType的类型改为jsonp就ok了

image

4.4 Json实现

4.4.1 客户端

使用jQuery。

我们说了jquery帮我们封装好了jsonp

4.4.2 服务端

  1. 接收callback参数,取回调的js的方法名。
  2. 业务逻辑处理。
  3. 响应结果,拼接一个js语句。

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author RickYinPeng
* @ClassName TokenController
* @Description 根据token查询用户信息
* @date 2018/12/17/18:24
*/
@Controller
public class TokenController {

@Autowired
private TokenService tokenService;

@RequestMapping(value = "/user/token/{token}",
produces= MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public String getUserByToken(@PathVariable String token,String callback){
E3Result result = tokenService.getUserByToken(token);
//响应结果之前先判断是否为jsonp请求
if(StringUtils.isNotBlank(callback)){
//把结果封装成一个js语句响应出去
return callback+"("+ JsonUtils.objectToJson(result)+");";
}
return JsonUtils.objectToJson(result);
}
}

produces= MediaType.APPLICATION_JSON_UTF8_VALUE,这个是将页面显示数据的格式设置为json,如果不设置我们返回的是一个Sting,它的显示格式是 text/什么(忘了) 显示的大,这样设置就好了。

4.4.3 Spring4之后的新方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping(value = "/user/token/{token}")
@ResponseBody
public Object getUserByToken(@PathVariable String token,String callback){
E3Result result = tokenService.getUserByToken(token);
//响应结果之前先判断是否为jsonp请求
if(StringUtils.isNotBlank(callback)){
//把结果封装成一个js语句响应出去
MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result);
mappingJacksonValue.setJsonpFunction(callback);
return mappingJacksonValue.toString();
}
return result;
}

这里我们使用了一个MappingJacksonValue这个对象,这是spring帮我们封装好的解决jsonp请求的对象

mappingJacksonValue.setJsonpFunction(callback);这是设置js语句