0%

单点登录-六-模块Sso-server

nacos安装完成,那么就需要在配置文件中配置,并且在nacos服务中新建命名空间,

1.首先在nacos中新建一个命名空间,用于注册服务到nacos中,

image-20201124141635630

2.引入当前需要的依赖,因为这是父子工程,所以可以直接放在主依赖中,然后其他模块直接引用,我是全部放在了Sso-server中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

3.添加配置文件,因为我喜欢yml文件格式,所以我将properties文件修改为了yml文件,这个可以根据自己的爱好,当然多个配置文件的时候要注意他们有不同的加载顺序,相同的配置会被覆盖,redis不设置哪个库的话,会默认使用0号库,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server:
port: 9001
spring:
application:
name: Sso-server
redis:
host: xxx.xxx.xxx.107
port: 6379
password: xxxxxx
cloud:
nacos:
discovery:
enabled: true
server-addr: 114.215.203.107:8848
namespace: 4c4dae70-45b6-4cd9-997d-5138800b41d2

thymeleaf:
prefix: classpath:/templates/ #prefix:指定模板所在的目录
check-template-location: true #check-tempate-location: 检查模板路径是否存在
cache: false #cache: 是否缓存,开发模式下设置为false,避免改了模板还要重启服务器,线上设置为true,可以提高性能。
suffix: .html
#encoding: UTF-8
#content-type: text/html
mode: HTML5

4.由于端口占用,我将端口修改为了9085,然后记得在启动器上加上注解@EnableDiscoveryClient,然后查看服务列表,

image-20201124143402690

5.使用同样的方式,只需要修改端口,服务名和命名空解,将模块System-b和模块System-gateway一起注册,Sso-server不用注册,它是作为认证中心的,

模块System-b:

image-20201124143653767

image-20201124151439700

模块System-gateway:

image-20201124143952162

image-20201124152603493

模块Sso-server:

image-20201127111848700

image-20201127112024455

6.我是在网上随便找了一个登录页面,放在resources/templates路径下面,因为这是Springboot默认扫描页面的路径,只有两个参数,用户名和密码,

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="//layui.hcwl520.com.cn/layui/css/layui.css?v=201811010202" rel="stylesheet">
</head>
<body style="margin-left: 25%;margin-right: 25%;margin-top:150px">

<div style="padding: 20px; background-color: #F2F2F2;">
<div class="layui-row layui-col-space15">
<div class="layui-card">
<fieldset class="layui-elem-field layui-field-title">
<legend>用户登录</legend>
</fieldset>
<form class="layui-form" method="post" action="/login">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="userName" lay-verify="title" autocomplete="off" placeholder="请输入标题"
class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" name="password" lay-verify="required" placeholder="请输入"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit="" lay-filter="demo1">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
<blockquote class="layui-elem-quote layui-text">
SSO登录演示:用户名和密码相同即可登录
</blockquote>
</div>
</div>
</div>
<script src="//layui.hcwl520.com.cn/layui/layui.js?v=201811010202"></script>
</body>
</html>

7.编写控制器,明确思路,a系统登录,监测到没有ticket,跳转到登录页面,编写登录页面跳转和登录控制器,并将源url和uuid都保存到cookie中,

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
   
/**
* 登录页面
* @param originalUrl 源url
* @param uuid 客户端唯一标识
* @param response
* @return
*/
@RequestMapping("/loginPage")
public String loginPage(String originalUrl, String uuid, HttpServletResponse response) {
SsoCookieUtil.setCookie(response, "originalUrl", originalUrl, ServerConstant.REDIS_TICKET_ALIVE_SECONDS);
SsoCookieUtil.setCookie(response, "uuid", uuid, ServerConstant.REDIS_TICKET_ALIVE_SECONDS);
return "login";
}



/**
* 登录验证
* @param userName
* @param password
* @param request
* @param response
* @return
*/
@ResponseBody
@RequestMapping("/login")
public boolean login(String userName, String password, HttpServletRequest request,HttpServletResponse response){
boolean loginSuccess = loginService.login(userName, password);
if (loginSuccess){
//获取客户端唯一标识
String uuid = SsoCookieUtil.getCookie(request, "uuid");
//创建ticket
String ticket = loginService.createTicket(uuid);
//存入redis,测试180秒过期
redisTemplate.opsForValue().set(ServerConstant.REDIS_TICKET_PREFIX + ticket,userName,ServerConstant.REDIS_TICKET_ALIVE_SECONDS, TimeUnit.SECONDS);
//回源url
String originalUrl = SsoCookieUtil.getCookie(request,"originalUrl") + "?ticket=" + ticket;
try {
//重定向到源url
response.sendRedirect(originalUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
return loginSuccess;
}

8.编写service层,主要是两个方法,一个是创建唯一的ticket值,和登录,

1
2
3
4
5
6
7
8
9
10
11
/**
* @Description:
* @Author: zllwsy
* @Date: 2020/11/4 11:09
*/
public interface LoginService {

String createTicket(String uuid);

boolean login(String userName,String password);
}

9.业务实现,创建ticket我随便采用了规制,SALT是EncryptUtil工具类里面定义的一个常量,为了时间有限,就没有连接数据库,而登录只要用户名和密码相等则登录成功,我相信连接数据库的难度应该都会吧,

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class LoginServiceImpl implements LoginService {

@Override
public String createTicket(String uuid) {
return DigestUtils.md5DigestAsHex((EncryptUtil.SALT+uuid+System.currentTimeMillis()).getBytes());
}

@Override
public boolean login(String userName, String password) {
return userName.equalsIgnoreCase(password);//用户名等于密码即可登录
}

10.EncryptUtil加密工具类,后面为了保证安全需要对cookie进行加密处理,

1
2
3
4
5
6
7
8
/**
* @Description:加密工具类
* @Author: zllwsy
* @Date: 2020/11/4 10:55
*/
public class EncryptUtil {
public static final String SALT = "1io10fdgadfjvower389fhday29834aguourfwpg0w82dllfkfadf";
}

11.设置服务的部分常量,其他应该都能看懂,说一下SSO_URO和COOKIE_DOMAIN,因为按照cookie的规范,一个cookie只能用于一个域名,不能发给其他的域名,doamin就代表了cookie所在的域,如网址为www.baidu.com/test,那么domain默认为www.baidu.com。而要实现跨域访问,如域A为aa.test.com,域B为bb.test.com,那么在域A生产一个令域A和域B都能访问的cookie就要将该cookie的domain设置为.test.com;如果要在域A生产一个令域A不能访问而域B能访问的cookie就要将该cookie的domain设置为bb.test.com,我这里是为了让A和B都能访问,所以要将域设置为test.com,而服务中心的路径则是SSO_URL,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @Description:服务常量
* @Author: zllwsy
* @Date: 2020/11/4 10:53
*/
public class ServerConstant {
public static final String REDIS_TICKET_PREFIX = "TICKET:";

public static final int REDIS_TICKET_ALIVE_SECONDS = 180;

public static final boolean ENABLE_DISPOSABLE_TICKET = false;

public static final String SSO_URL = "http://sso.com:9088/";

public static final String COOKIE_DOMAIN = "test.com";
}

12.编写一个获取cookie和保存cookie的工具类,

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
/**
* @Description:SsoCookie服务工具类
* @Author: zllwsy
* @Date: 2020/11/4 10:54
*/
public class SsoCookieUtil {
/**
*
* @param request
* @param cookieName
* @return
*/
public static String getCookie(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
}
return null;
}

/**
*
* @param response
* @param cookieName
* @param value
* @param maxAge
*/
public static void setCookie(HttpServletResponse response, String cookieName, String value,int maxAge) {
Cookie cookie = new Cookie(cookieName, value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}

/**
* 设置doMainCookie
* @param response
* @param cookieName
* @param value
* @param maxAge
*/
public static void setDomainCookie(HttpServletResponse response,String cookieName,String value,int maxAge){
Cookie cookie = new Cookie(cookieName, value);
cookie.setDomain(ServerConstant.COOKIE_DOMAIN);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
}

13.编写拦截器,在认证中心配置拦截器的预处理,而其他系统模块直接拦截请求然后转发到这里就行了,逻辑其实已经说了很多次,并且代码进行了大量注释,

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
/**
* @Description:配置拦截器
* @Author: zllwsy
* @Date: 2020/11/4 10:19
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);
@Autowired
private RedisTemplate<String, String> redisTemplate;

public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception{
//客户端唯一标识uuid,客户端第一次访问时的session作为唯一标识,如果为空则写入cookie中
HttpSession session = request.getSession();
String uuid = SsoCookieUtil.getCookie(request, "uuid");
if (StringUtils.isEmpty(uuid)){
//如果uuid为空,则将session的id设置为uuid
uuid = session.getId();
//将uuid存储到cookie中
SsoCookieUtil.setDomainCookie(response,"uuid",uuid,3600);
}
//从cookie中获取ticket
String ticket = SsoCookieUtil.getCookie(request, "ticket");
// String ticket = null;
// if (aesDecode != null){
// ticket = EncryptUtil.AESDecode(aesDecode);
// }

//如果cookie中没有获取到,则从请求中去获取
if (StringUtils.isEmpty(ticket)){
// String oldTicketValue = redisTemplate.opsForValue().get(ServerConstant.REDIS_TICKET_PREFIX + ticket);
// System.out.println(oldTicketValue);
ticket = request.getParameter("ticket");
}
//如果ticket为空
if (StringUtils.isEmpty(ticket)){
logger.debug("非法请求:未获取到ticket,重定向到登录页面...");
//由于没有ticket,就重定向认证服务的登录接口
response.sendRedirect(ServerConstant.SSO_URL + "loginPage?originalUrl=" + request.getRequestURL() + "&uuid=" + uuid);
// String s = Contstant.SSO_URL + "loginPage?originalUrl=" + request.getRequestURL() + "&uuid=" + uuid;
// System.out.println(s);
return false;
}else {
RestTemplate restTemplate = new RestTemplate();
//验证ticket,校验成功返回ticket,实际上校验成功时可以返回用户信息,校验失败则返回null
String t = restTemplate.getForObject(ServerConstant.SSO_URL + "checkTicket?ticket=" + ticket + "&uuid=" + uuid, String.class);
if (t != null){
//通过restTemplate来发送内部http请求,获取用户信息,然后存储到map中
Map userInfo = restTemplate.getForObject(ServerConstant.SSO_URL + "getUserInfo?ticket=" + t, Map.class);
logger.debug("ticket验证通过:",userInfo);
//userInfo存入到session中
session.setAttribute("userInfo",userInfo);
//更新ticket
// String aesEncode = EncryptUtil.AESEncode(ticket);
// CookieUtil.setCookie(response,"aesEncode",t,60);
SsoCookieUtil.setDomainCookie(response,"ticket",t,60);

}else {
logger.debug("非法请求:路径错误,重定向到登录页面...");
response.sendRedirect(ServerConstant.SSO_URL + "loginPage?originalUrl=" + request.getRequestURL() + "&uuid=" + uuid);
return false;
}
}
return true;
}
}

14.第一次登录的时候,uuid和ticket全都为空,uuid被设置为sesssion的id,ticket为空,直接非法请求,然后重定向到刚刚的登录页面,也就是发送login请求,

image-20201128143702261

15.使用用户名和密码登录后,ticket被创建并且存储到redis中设置过期时间,然后再重定向到源url,源url其实就是系统登录的路径,

image-20201128143925002

16.重定向到了源url,同样还是被拦截器拦截,再一次来到LoginInterceptor类,这个时候uuid有了,ticket有了,开始验证,并存储用户信息,更新cookie,使用的restTemplate来发送请求,

image-20201128144157349

17.进入校验请求,检查ticket和存储在redis中的ticket,如果相同,则删除旧的ticket,重新生成新的ticket,并且刷新redis的存活时间,

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
/**
* 校验ticket
* @param ticket
* @param uuid
* @return
*/
@ResponseBody
@RequestMapping("/checkTicket")
public String checkTicket(String ticket,String uuid){
String oldTicketValue = redisTemplate.opsForValue().get(ServerConstant.REDIS_TICKET_PREFIX + ticket);
//较验ticket
//当redis存在ticket时验证成功
if (ticket != null && oldTicketValue != null) {
//注意:开启一次性ticket验证时,uuid和ticket必传
if (ServerConstant.ENABLE_DISPOSABLE_TICKET) {
//清除旧的ticket
redisTemplate.delete(ServerConstant.REDIS_TICKET_PREFIX + ticket);
//生成新的ticket
String newTicket = loginService.createTicket(uuid);
//保存新的ticket
redisTemplate.opsForValue().set(ServerConstant.REDIS_TICKET_PREFIX + newTicket, oldTicketValue, ServerConstant.REDIS_TICKET_ALIVE_SECONDS, TimeUnit.SECONDS);
//返回新ticket
return newTicket;
} else {
return ticket;
}
} else return null;
}

18.校验成功,t为新的ticket,再发送请求,来获取用户信息,存储到map中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 获取用户信息
* @param ticket
* @return
*/
@ResponseBody
@RequestMapping("/getUserInfo")
public Map<String, String> getUserInfo(String ticket) {
//模拟根据ticket获取用户用户名,根据用户名获取用户信息
String userName = redisTemplate.opsForValue().get(ServerConstant.REDIS_TICKET_PREFIX + ticket);
if (StringUtils.isEmpty(userName)) {
return null;
}
Map<String, String> userInfo = new HashMap<>();
userInfo.put("userName", userName);
userInfo.put("ticket", ticket);
return userInfo;
}

19.使用getUserInfo接口获取要用户信息,回到拦截器,将其存储到session中,然后更新cookie,注意这里需要使用设置了域的cookie方法,到此,登录成功,其他系统登录的时候,因为cookie中存在ticket和uuid就可以直接走保存用户信息刷新存活时间的路径,

image-20201128144852483

总结:

其实确实有点乱,我想干干净净的写每一个模块,而实际开发的时候都是相互交叉的,并不是说写了认证中心,再写A,并且我是实现了功能再开始写,着实有点乱,请见谅,后面会给出所有源码的地址。

----------本文结束感谢您的阅读----------