nacos安装完成,那么就需要在配置文件中配置,并且在nacos服务中新建命名空间,
1.首先在nacos中新建一个命名空间,用于注册服务到nacos中,

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/ check-template-location: true cache: false suffix: .html mode: HTML5
|
4.由于端口占用,我将端口修改为了9085,然后记得在启动器上加上注解@EnableDiscoveryClient,然后查看服务列表,

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


模块System-gateway:


模块Sso-server:


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
|
@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"; }
@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"); String ticket = loginService.createTicket(uuid); redisTemplate.opsForValue().set(ServerConstant.REDIS_TICKET_PREFIX + ticket,userName,ServerConstant.REDIS_TICKET_ALIVE_SECONDS, TimeUnit.SECONDS); String originalUrl = SsoCookieUtil.getCookie(request,"originalUrl") + "?ticket=" + ticket; try { response.sendRedirect(originalUrl); } catch (IOException e) { e.printStackTrace(); } } return loginSuccess; }
|
8.编写service层,主要是两个方法,一个是创建唯一的ticket值,和登录,
1 2 3 4 5 6 7 8 9 10 11
|
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
|
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
|
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
|
public class SsoCookieUtil {
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; }
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); }
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
|
@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{ HttpSession session = request.getSession(); String uuid = SsoCookieUtil.getCookie(request, "uuid"); if (StringUtils.isEmpty(uuid)){ uuid = session.getId(); SsoCookieUtil.setDomainCookie(response,"uuid",uuid,3600); } String ticket = SsoCookieUtil.getCookie(request, "ticket");
if (StringUtils.isEmpty(ticket)){
ticket = request.getParameter("ticket"); } if (StringUtils.isEmpty(ticket)){ logger.debug("非法请求:未获取到ticket,重定向到登录页面..."); response.sendRedirect(ServerConstant.SSO_URL + "loginPage?originalUrl=" + request.getRequestURL() + "&uuid=" + uuid);
return false; }else { RestTemplate restTemplate = new RestTemplate(); String t = restTemplate.getForObject(ServerConstant.SSO_URL + "checkTicket?ticket=" + ticket + "&uuid=" + uuid, String.class); if (t != null){ Map userInfo = restTemplate.getForObject(ServerConstant.SSO_URL + "getUserInfo?ticket=" + t, Map.class); logger.debug("ticket验证通过:",userInfo); session.setAttribute("userInfo",userInfo);
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请求,

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

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

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
|
@ResponseBody @RequestMapping("/checkTicket") public String checkTicket(String ticket,String uuid){ String oldTicketValue = redisTemplate.opsForValue().get(ServerConstant.REDIS_TICKET_PREFIX + ticket); if (ticket != null && oldTicketValue != null) { if (ServerConstant.ENABLE_DISPOSABLE_TICKET) { redisTemplate.delete(ServerConstant.REDIS_TICKET_PREFIX + ticket); String newTicket = loginService.createTicket(uuid); redisTemplate.opsForValue().set(ServerConstant.REDIS_TICKET_PREFIX + newTicket, oldTicketValue, ServerConstant.REDIS_TICKET_ALIVE_SECONDS, TimeUnit.SECONDS); 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
|
@ResponseBody @RequestMapping("/getUserInfo") public Map<String, String> getUserInfo(String 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就可以直接走保存用户信息刷新存活时间的路径,

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