sso
本文最后更新于:9 个月前
SSO
SSO(Single Sign-On)单点登录。它是一种身份验证和授权机制,允许用户使用一组凭据(如用户名和密码)登录到一个应用程序,然后在不再需要重新输入凭据的情况下,访问其他已经集成了SSO的应用程序。
以下是SSO的主要特点和优势:
- 单一身份验证: 用户只需一次登录,即可访问多个不同的应用程序和服务,而无需为每个应用程序都提供凭据。
- 降低用户认证负担: SSO减少了用户需要记住的用户名和密码数量,降低了用户认证的负担,提高了用户体验。
- 中心化身份管理: SSO通常包括一个中心身份提供者(Identity Provider,IdP),负责管理用户身份和权限。这简化了身份和权限管理,使得可以更容易地添加、修改和删除用户。
- 增强安全性: 尽管只需要一次登录,但SSO可以提供强大的安全性,包括多因素身份验证(MFA)和单点注销(Single Log-Out)等功能,以确保用户数据和应用程序的安全性。
- 集成多种协议: SSO可以支持多种身份验证和授权协议,如SAML(Security Assertion Markup Language)、OAuth、OpenID Connect等,以适应不同应用程序和服务的需求。
例如你登录网易账号中心(https://reg.163.com/ )之后访问以下站点都是登录状态。
- 网易直播 https://v.163.comopen in new window
- 网易博客 https://blog.163.comopen in new window
- 网易花田 https://love.163.comopen in new window
- 网易考拉 https://www.kaola.comopen in new window
- 网易 Lofter http://www.lofter.comopen in new window
那么如何动手实现一个SSO框架?
SSO-demo
创建工程
首先创建maven父工程,其pom.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>sso_demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<relativePath/>
<version>2.6.7</version>
</parent>
<modules>
<module>sso-vip</module>
<module>sso-cart</module>
<module>sso-main</module>
<module>sso-login</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
</project>
可以看到,其有三个子工程sso-vip、sso-login、sso-cart、sso-main,都为Spring-boot项目,提供网页服务
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sso_demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sso-login</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
这四个模块在一起就很像电商平台的四个微服务,分别运行在不同的端口下。
其中main运行在
server | port | url |
---|---|---|
sso-main | 9010 | www.codeshop.com:9010 |
sso-vip | 9011 | vip.codeshop.com:9011 |
sso-cart | 9012 | cart.codeshop.com:9012 |
sso-login | 9000 | login.codeshop.com:9000 |
在hosts文件中添加一下内容
sudo vim /etc/hosts
127.0.0.1 www.codeshop.com
127.0.0.1 vip.codeshop.com
127.0.0.1 cart.codeshop.com
127.0.0.1 login.codeshop.com
访问时记得清除浏览器缓存,不然可能访问不到。
代码逻辑
非登录模块
当用户访问www.codeshop.com:9010时其可以不登录账号,但是访问vip.codeshop.com:9011、 cart.codeshop.com:9012则必须要跳转到login.codeshop.com:9000,进行登录操作。如下图所示逻辑。
那么前端代码逻辑就是
- 首先判断用户是否登录
- 如果用户登录,则从session中获取保存的用户信息,如用户名
- 如果用户没登录,则需要跳转到登录页面
- 在用户登录完成后,需要返回到原来的页面,并且显示登录的用户信息,所以在访问登录页面时,需要携带参数,用于跳转回原来页面
下面以vip页面为例,其他页面同理
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>VIP模块</title>
</head>
<body>
<h1>VIP服务!</h1>
<span>
<a th:if="${session.loginUser == null}" href="http://login.codeshop.com:9000/view/login?target=http://vip.codeshop.com:9011/view/index">login</a>
<a th:unless="${session.loginUser == null}" href="http://vip.codeshop.com:9011/view/logout?target=http://vip.codeshop.com:9011/view/index">logout</a>
</span>
<p th:unless="${session.loginUser == null}">
<span style="color: cadetblue" th:text="${session.loginUser.username}"></span> 登录成功! <br></br>
<br></br>
用户id: <span style="color:cadetblue" th:text="${session.loginUser.id}"></span><br></br>
用户名: <span style="color:cadetblue" th:text="${session.loginUser.username}"></span><br></br>
</p>
</body>
</html>
后端代码如下
@Controller
@RequestMapping("/view")
public class ViewController {
@Autowired
private RestTemplate restTemplate;
private final String USER_INFO_ADDRESS = "http://login.codeshop.com:9000/login/info?token=";
@GetMapping("/index")
public String toIndex(@CookieValue(required = false, value = "TOKEN") Cookie cookie,
HttpSession session) {
if (cookie != null) {
String token = cookie.getValue();
if (!StringUtils.isEmpty(token)) {
Map result = restTemplate.getForObject(USER_INFO_ADDRESS + token, Map.class);
session.setAttribute("loginUser", result);
}
}
return "index";
}
}
其中RestTemplate的作用是用于http调用,在微服务框架中,每个服务独立,所以vip模块无法直接调用login的函数,所以使用http进行调用
在启动类上要进行Bean的注册
@SpringBootApplication
public class VipApp {
public static void main(String[] args) {
SpringApplication.run(VipApp.class,args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
登录模块
前段页面,只要提供两个输入框,接受用户名和密码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Login Module</title>
</head>
<body>
<h1>用户登录 </h1>
<p style="color: red;" th:text="${session.msg}"></p>
<form action="/login" method="POST">
姓名: <input name="username" value="">
<br></br>
密码: <input name="password" value="">
<br></br>
<button type="submit" style="width:60px;height:30px"> login </button>
</form>
</body>
</html>
创建用户实体,用于模拟
@Data //添加getter/setter
@NoArgsConstructor //添加无参构造器
@AllArgsConstructor //添加全参构造器
@Accessors(chain = true) //添加链式调用
public class User {
private Integer id;
private String username;
private String password;
}
控制器代码逻辑http://login.codeshop.com:9000/view/login
- 首先判断是否有target参数,如果没有跳转到
login.codeshop.com:9010
进行登录 - 然后判断是否请求的cookie中是否携带token
- 如果携带token,用户已完成登录,从缓存中获取用户信息返回即可
- 没有携带token则需要
/login
下完成登录
@Controller
@RequestMapping("/view")
public class ViewController {
/**
* 跳转到登录页面
* @return
*/
@GetMapping("/login")
public String toLogin(@RequestParam(required = false, defaultValue = "") String target,
HttpSession session,
@CookieValue(required = false, value = "TOKEN") Cookie cookie){
if(StringUtils.isEmpty(target)){
target = "login.codeshop.com:9010";
}
if(cookie != null){
String value = cookie.getValue();
User user = LogCacheUtil.loginUser.get(value);
if(user != null){
return "redirect:" + target;
}
}
session.setAttribute("target",target);
return "login";
}
}
/login
登录模块的代码
@Controller
@RequestMapping("/login")
public class LoginController {
private static Set<User> dbUser;
// 模拟用户数据
static {
dbUser = new HashSet<>();
dbUser.add(new User(1, "adx", "123456"));
dbUser.add(new User(2, "admin", "123456"));
}
@PostMapping
public String doLogin(User user, HttpSession session, HttpServletResponse response) {
String target = (String) session.getAttribute("target");
Optional<User> first = dbUser.stream().filter(dbUser -> dbUser.getUsername().equals(user.getUsername()) &&
dbUser.getPassword().equals(user.getPassword()))
.findFirst();
if (first.isPresent()) {
// 使用随机数作为token
String token = UUID.randomUUID().toString();
Cookie cookie = new Cookie("TOKEN", token);
cookie.setPath("/");
// 设置token的作用域名
cookie.setDomain("codeshop.com");
// 在响应头中添加cookie
response.addCookie(cookie);
// 保存已登录用户信息
LogCacheUtil.loginUser.put(token, first.get());
} else {
session.setAttribute("msg", "用户名或密码错误!");
return "login";
}
return "redirect:" + target;
}
/**
根据token获取用户在缓存中的信息
*/
@GetMapping("info")
@ResponseBody
public ResponseEntity<User> getUserInfo(String token) {
if (!StringUtils.isEmpty(token)) {
User user = LogCacheUtil.loginUser.get(token);
return ResponseEntity.ok(user);
} else {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
}
/**
退出操作
*/
@GetMapping("/logout")
public String loginOut(@CookieValue(value = "TOKEN") Cookie cookie, HttpServletResponse response, String target) {
// 将cookie的过期时间修改为0
cookie.setMaxAge(0);
// 从缓存中删除用户的信息
LogCacheUtil.loginUser.remove(cookie.getValue());
response.addCookie(cookie);
return "redirect:" + target;
}
}
后端响应后,返回本次登录的cookie,前端在获取cookie后,会将信息保存到浏览器中。
此时登录的是www.codeshop.com
当访问vip.codeshop.com时,就会发现,用登录也可以获取到登录用户的信息。
使用redis缓存用户token
整合redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在父工程的pom文件中添加依赖,版本与使用springboot保持一致
添加操作redis的工具类,注意这个不添加也不影响,添加了后,使用redis更加方便简捷,不添加的话可以直接使用RedisTemplate进行操作,效果是一样的
@Component
public class RedisUtils {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 读取缓存
*
* @param key
* @return
*/
public String get(final String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 写入缓存
*/
public boolean set(final String key, String value) {
boolean result = false;
try {
redisTemplate.opsForValue().set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存,并设置过期时间
*
* @param key
* @param value
* @param timeout
* @param unit
* @return
*/
public boolean set(final String key, String value, long timeout, TimeUnit unit) {
boolean result = false;
try {
redisTemplate.opsForValue().set(key, value, timeout, unit);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 更新缓存
*/
public boolean getAndSet(final String key, String value) {
boolean result = false;
try {
redisTemplate.opsForValue().getAndSet(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 删除缓存
*/
public boolean delete(final String key) {
boolean result = false;
try {
redisTemplate.delete(key);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
修改登录模块从缓存中获取token的代码
@GetMapping("/login")
public String toLogin(@RequestParam(required = false, defaultValue = "") String target,
HttpSession session,
@CookieValue(required = false, value = "TOKEN") Cookie cookie){
if(StringUtils.isEmpty(target)){
target = "login.codeshop.com:9010";
}
if(cookie != null){
String token = cookie.getValue();
String username = redisUtils.get(token); // 从redis中根据token获取用户信息
System.out.println(username);
// User user = LogCacheUtil.loginUser.get(token);
if(user != null){
return "redirect:" + target;
}
}
session.setAttribute("target",target);
return "login";
}
在用户登录时,以<token, username>为键值对保存信息。
@PostMapping
public String doLogin(User user, HttpSession session, HttpServletResponse response) {
String target = (String) session.getAttribute("target");
Optional<User> first = dbUser.stream().filter(dbUser -> dbUser.getUsername().equals(user.getUsername()) &&
dbUser.getPassword().equals(user.getPassword()))
.findFirst();
if (first.isPresent()) {
String token = UUID.randomUUID().toString();
Cookie cookie = new Cookie("TOKEN", token);
cookie.setPath("/");
cookie.setDomain("codeshop.com");
response.addCookie(cookie);
// 向redis中存token
redisUtils.set(token, first.get().getUsername());
// LogCacheUtil.loginUser.put(token, first.get());
} else {
session.setAttribute("msg", "用户名或密码错误!");
return "login";
}
return "redirect:" + target;
}
@GetMapping("info")
@ResponseBody
public ResponseEntity<String> getUserInfo(String token) {
if (!StringUtils.isEmpty(token)) {
String username = redisUtils.get(token);
// User user = LogCacheUtil.loginUser.get(token);
return ResponseEntity.ok(username);
} else {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
}
在main, vip, cart模块也要修改获取用户信息的代码
@Autowired
private RedisUtils redisUtils;
@Autowired
private RestTemplate restTemplate;
//private final String USER_INFO_ADDRESS = "http://login.codeshop.com:9000/login/info?token=";
@GetMapping("/index")
public String toIndex(@CookieValue(required = false, value = "TOKEN")Cookie cookie,
HttpSession session){
if(cookie != null){
String token = cookie.getValue();
if(!StringUtils.isEmpty(token)){
String result = redisUtils.get(token); // 直接从redis中获取,省去http调用带来的的延时
session.setAttribute("loginUser",result);
}
}
return "index";
}
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!