单点登录(SSO):SSO是指在多个应用系统中个,用户只需要登陆一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一用户的登陆的机制。简单的说登录一次之后的子系统或关联项目则无需再次登录。实现原理
单点登录工作原理
1.用户从浏览器输入需要请求的资源,如http://www.user.com/user/wel
2.请求到达后端服务之后http://www.user.com/user/wel发现没有携带jsessionid信息
3.资源服务器将请求转发到sso服务器并写单回调地址
4.认证服务接到请求之后响应浏览器一个登陆表单
5.用户在浏览器中输入账号密码提交到认证服务器
6.认证服务器校验账号密码如果失败则返回到第4步,服务端生产全局jsessionid并请求回调地址
7.回调所在资源接收到请求之后,发现携带有jsessionid则复制一份保存在当前服务器中将资源返回给浏览器
8.如果用户请求其他系统资源如订单模块
9.此时在认证项目中发现有jsessionid则直接回调订单模块资源地址,并将jsessionid保存到服务器中
10.用户无需登录就可直接访问资源
项目目录结构
├─src
│ ├─main
│ │ ├─java
│ │ └─resources
├─sso-client-order
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─mrduan
│ │ │ │ └─order
│ │ │ │ ├─config
│ │ │ │ ├─controller
│ │ │ │ ├─interceptor
│ │ │ │ └─util
│ │ │ └─resources
│ │ │ ├─static
│ │ │ └─templates
├─sso-client-user
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─mrduan
│ │ │ │ └─user
│ │ │ │ ├─config
│ │ │ │ ├─controller
│ │ │ │ ├─interceptor
│ │ │ │ └─util
│ │ │ └─resources
│ │ │ ├─static
│ │ │ └─templates
└─sso-server
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─mrduan
│ │ │ └─ssoserver
│ │ │ ├─controller
│ │ │ └─util
│ │ └─resources
│ │ ├─static
│ │ │ └─images
│ │ └─templates
项目依赖
sso pom.xml
4.0.0
com.mrduan
sso
pom
1.0-SNAPSHOT
sso-server
sso-client-order
sso-client-user
sso-server
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
2.2.0.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-thymeleaf
com.auth0
java-jwt
3.2.0
org.springframework.boot
spring-boot-maven-plugin
服务端代码
登录控制器SSOController
package com.mrduan.ssoserver.controller;
import com.mrduan.ssoserver.util.JwtUtil;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
@Controller
public class SSOController {
List list = new ArrayList<>();
/**
* 预登录
* @param url
* @param request
* @param model
* @return
*/
@RequestMapping(value = "/preLogin",method = RequestMethod.GET)
public String preLogin(String url, HttpServletRequest request, Model model){
System.out.println("请求的地址:" + url);
HttpSession session = request.getSession(false);
// 如果没有登录则跳转到登录页面
if(session == null){
model.addAttribute("url",url);
return "login";
}
else{
String token = (String)session.getAttribute("token");
return "redirect:http://"+url+"?token="+token;
}
}
@RequestMapping("/login")
public String login(String username, String password, String url, HttpServletRequest request){
// TODO 登录这里写死的,需要与数据库等整合的在修改这里即可
if("Bruce".equals(username) && "123456".equals(password)){
// 创建token
String token = null;
try {
token = JwtUtil.createJwt();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println("token信息"+token);
HttpSession session = request.getSession();
request.getSession().setAttribute("token",token);
list.add(token);
return "redirect:http://"+url;
}else{
return "login";
}
}
@RequestMapping("/checkToken")
@ResponseBody
public String checkToken(String token) {
System.out.println(JwtUtil.verifyJwt(token));
if (list.contains(token)&&JwtUtil.verifyJwt(token)) {
return "correct";
}
return "incorrect";
}
}
token创建工具
package com.mrduan.ssoserver.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.io.UnsupportedEncodingException;
import java.util.Date;
public class JwtUtil {
/**
* 创建JWT
* @return
* @throws UnsupportedEncodingException
*/
public static String createJwt() throws UnsupportedEncodingException {
Algorithm al = Algorithm.HMAC256("secretkey");
// 这里也是写死的,需要与数据库整合的修改这里即可
String token = JWT.create()
.withIssuer("Burce")
.withSubject("SSO")
.withClaim("userid","1234")
.withExpiresAt(new Date(System.currentTimeMillis()+360000))
.sign(al);
return token;
}
/**
* 验证jwt
* @param token
* @return
*/
public static boolean verifyJwt(String token){
try {
Algorithm algorithm = null;
algorithm = Algorithm.HMAC256("secretkey");
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (JWTVerificationException e){
e.printStackTrace();
System.out.println("校验失败");
}
return false;
}
}
项目启动类
package com.mrduan.ssoserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SsoServerApplication {
public static void main(String[] args) {
SpringApplication.run(SsoServerApplication.class,args);
}
}
配置文件
server:
port: 8090
到此服务端的代码编写完毕
注意:前端代码没有在这里贴出来,如有需要,文章下方有资源链接
用户模块代码
项目sso-clinet-user
视图控制器类UserController
package com.mrduan.user.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/wel")
public String wel(){
return "wel";
}
}
session拦截器
package com.mrduan.user.interceptor;
import com.mrduan.user.util.HttpUtil;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class SessionInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
// 判断session是否有jsessionid
if(session!=null && session.getAttribute("login").equals("login")){
return true;
}
String token = request.getParameter("token");
System.out.println("得到token信息"+token);
//当token信息不为空的是
if(token != null){
String reqUrl = "http://www.sso.com:8090/checkToken";
String content = "token="+token;
String result = HttpUtil.sendReq(reqUrl,content);
if ("correct".equals(result)) {
// token有效
// 子系统的局部session对象
request.getSession().setAttribute("login", "login");
return true;
}
}
// www.user.com:8081/user/wel 发现既没有jsessionid也没有token,只能做页面的跳转,到认证中心去认证
response.sendRedirect("http://www.sso.com:8090/preLogin?url=www.user.com:8081/user/wel");
return false;
}
}
资源请求工具类
package com.mrduan.user.util;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpUtil {
public static String sendReq(String reqUrl,String content) throws Exception {
URL url=new URL(reqUrl);
HttpURLConnection conn= (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
byte[] b=content.getBytes("UTF-8");
conn.getOutputStream().write(b);
conn.connect();
InputStream in =conn.getInputStream();
return Stream2String(in);
}
public static String Stream2String(InputStream in){
if(in!=null) {
try {
BufferedReader tBufferedReader = new BufferedReader(new InputStreamReader(in));
StringBuffer tStringBuffer = new StringBuffer();
String sTempOneLine = new String("");
while ((sTempOneLine = tBufferedReader.readLine()) != null) {
tStringBuffer.append(sTempOneLine);
}
return tStringBuffer.toString();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}
拦截器配置类
package com.mrduan.user.config;
import com.mrduan.user.interceptor.SessionInterceptor;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootConfiguration
public class UserWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截地址为/user/*
registry.addInterceptor(sessionInterceptor()).addPathPatterns("/user/*");
}
@Bean
public SessionInterceptor sessionInterceptor(){
return new SessionInterceptor();
}
}
项目启动类
package com.mrduan.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserSsoClientApplication{
public static void main(String[] args) {
SpringApplication.run(UserSsoClientApplication.class,args);
}
}
项目配置文件
server:
port: 8081
注意:前端代码没有在这里贴出来,如有需要,文章下方有资源链接
订单模块代码
订单模块的代码与用户模块代码几乎一致
项目sso-clinet-user
视图控制器类OrderController
package com.mrduan.order.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("order")
//www.order.com:8082/order/wel
public class OrderController {
@RequestMapping("wel")
public String wel(){
return "wel";
}
}
session拦截器
package com.mrduan.order.interceptor;
import com.mrduan.order.util.HttpUtil;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class SessionInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("I'm here");
HttpSession session =request.getSession(false);
if (session!=null&&session.getAttribute("login").equals("login")) {
return true;
}
String token = request.getParameter("token");
if (token != null) {
String reqUrl = "http://www.sso.com:8090/checkToken";
String content = "token=" + token;
String result = HttpUtil.sendReq(reqUrl, content);
if ("correct".equals(result)) {
request.getSession().setAttribute("login", "login");
return true;
}
}
response.sendRedirect("http://www.sso.com:8090/preLogin?url=www.order.com:8082/order/wel");
return false;
}
}
资源请求工具类
package com.mrduan.order.util;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpUtil {
public static String sendReq(String reqUrl,String content) throws Exception {
URL url=new URL(reqUrl);
HttpURLConnection conn= (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
byte[] b=content.getBytes("UTF-8");
conn.getOutputStream().write(b);
conn.connect();
InputStream in =conn.getInputStream();
return Stream2String(in);
}
public static String Stream2String(InputStream in){
if(in!=null) {
try {
BufferedReader tBufferedReader = new BufferedReader(new InputStreamReader(in));
StringBuffer tStringBuffer = new StringBuffer();
String sTempOneLine = new String("");
while ((sTempOneLine = tBufferedReader.readLine()) != null) {
tStringBuffer.append(sTempOneLine);
}
return tStringBuffer.toString();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}
拦截器配置类
package com.mrduan.order.config;
import com.mrduan.order.interceptor.SessionInterceptor;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootConfiguration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getSessionInterceptor()).addPathPatterns("/order/*");
}
@Bean
public SessionInterceptor getSessionInterceptor(){
return new SessionInterceptor();
}
}
项目启动类
package com.mrduan.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SsoOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SsoOrderApplication.class,args);
}
}
项目配置文件
server:
port: 8082
注意:前端代码没有在这里贴出来,如有需要,文章下方有资源链接
到此我我们代码部分编写完毕
配置DNS解析
我们这里使用的是域名访问的因此需要配置host文件
mac环境配置
sudo vi /etc/hosts
在最后面追加3行
127.0.0.1 www.sso.com
127.0.0.1 www.user.com
127.0.0.1 www.order.com
windows环境配置
文件位置
C:\Windows\System32\drivers\etc
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host
# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
127.0.0.1 www.sso.com
127.0.0.1 www.user.com
127.0.0.1 www.order.com
新增三行记录
127.0.0.1 www.sso.com
127.0.0.1 www.user.com
127.0.0.1 www.order.com
测试验证
到此配置也完成,我们分别启动这三台服务器测试一下效果
在浏览器输入http://www.user.com:8081/user/wel
输入之后由于我们没有登录则直接跳转到登录页面了
http://www.sso.com:8090/preLogin?url=www.user.com:8081/user/wel
并且携带了我们先去输入的地址
我们输入用户名和密码
Bruce/123456
登录之后可以发现,地址重新回到了www.user.com:8081/user/wel,并且携带了token信息
接下来我们尝试访问订单模块
在浏览器输入http://www.order.com:8082/order/wel
这一次无需再次登录直接定位到了资源页面
对比token发现这2个页面的token是一样的
在浏览器中查看cookie信息可以看到jsessionid信息
同时在www.sso.com域下也产生了一个cookie
到此我们的验证也完成
项目源代码
视频教程