最近在自学 Java Web 开发, 听到公司 .Net 组经常说到一个词, 单点登录, 于是乎想着自己是否能用 Java 来实现一下, 于是有了下面的文章。
单点登录
简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
我们需要创建的目标
我们要创建3个独立的系统
- 1 个认证系统: 将被部署在
localhost:8080
- 2 个资源系统(为了简化, 我们使用相同的代码): 将分别被部署在
localhost:8180
和localhost:8280
我要需要的环境
- JDK 1.7+
- Maven 3+
用到的技术栈
- Java
- Single Sign On (SSO)
- Json Web Token (Jwt)
- Spring Boot
- Freemarker
关于什么是 JWT, 这里推荐下面几篇文章, 讲解的比较清楚.
- 八幅漫画理解使用JSON Web Token设计单点登录系统
- JSON Web Token Tutorial with Example in Python
- Crafting your way through JSON Web Tokens
认证系统
项目结构
.
├── src
│ └─ main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── sso
│ │ ├── SsoApplication.java
│ │ └── auth
│ │ ├── CookieUtil.java
│ │ ├── JwtUtil.java
│ │ └── LoginController.java
│ ├── resources
│ │ └── application.properties
│ └── webapp
│ └── login.ftl
└── pom.xml
项目依赖
pom.xml1
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"1.0" encoding="UTF-8" xml version=
<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>com.example</groupId>
<artifactId>sso</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>sso</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
CookieUtil
Jwt Token 将通过 Cookies 保存和提取
src/main/java/com/example/sso/auth/CookieUtil.java1
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
31package com.example.sso.auth;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CookieUtil {
public static void create(HttpServletResponse httpServletResponse, String name, String value, Boolean secure, Integer maxAge, String domain) {
Cookie cookie = new Cookie(name, value);
cookie.setSecure(secure);
cookie.setHttpOnly(true);
cookie.setMaxAge(maxAge);
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
}
public static void clear(HttpServletResponse httpServletResponse, String name) {
Cookie cookie = new Cookie(name, null);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
httpServletResponse.addCookie(cookie);
}
public static String getValue(HttpServletRequest httpServletRequest, String name) {
Cookie cookie = WebUtils.getCookie(httpServletRequest, name);
return cookie != null ? cookie.getValue() : null;
}
}
cookie.setSecure(secure)
: secure=true => 仅仅能在 HTTPS 连接中被浏览器传递到服务器端进行会话验证.cookie.setHttpOnly(true)
: 使得 Javascript 脚本不能读取 cookies.cookie.setMaxAge(maxAge)
: 设置 Cookies 的过期值. maxAge=0 => 立即过期, maxAge=-1 => 永不过期cookie.setDomain(domain)
: Cookies 仅对设置的域名可见.cookie.setPath("/")
: Cookies 对所有路径可见.
JwtUtil
我们使用 JJWt 来生成和解析 JWT Token.
src/main/java/com/example/sso/auth/JwtUtil.java1
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
29package com.example.sso.auth;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
public class JwtUtil {
public static String generateToken(String signingKey, String subject) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, signingKey);
return builder.compact();
}
public static String getSubject(HttpServletRequest httpServletRequest, String jwtTokenCookieName, String signingKey) {
String token = CookieUtil.getValue(httpServletRequest, jwtTokenCookieName);
if (token == null) return null;
return Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token).getBody().getSubject();
}
}
LoginController
src/main/java/com/example/sso/auth/LoginController.java1
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
46package com.example.sso.auth;
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 javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class LoginController {
private static final String jwtTokenCookieName = "JWT-TOKEN";
private static final String signingKey = "signingKey";
private static final Map<String, String> credentials = new HashMap<>();
public LoginController() {
credentials.put("hellokoding", "hellokoding");
credentials.put("hellosso", "hellosso");
}
"/") (
public String home() {
return "redirect:/login";
}
"/login") (
public String login() {
return "login";
}
"login", method = RequestMethod.POST) (value =
public String login(HttpServletResponse httpServletResponse, String username, String password, String redirect, Model model) {
if (username == null || !credentials.containsKey(username) || !credentials.get(username).equals(password)) {
model.addAttribute("error", "Invalid username or password!");
return "login";
}
String token = JwtUtil.generateToken(signingKey, username);
CookieUtil.create(httpServletResponse, jwtTokenCookieName, token, false, -1, "localhost");
return "redirect:" + redirect;
}
}
为了简化, 我们使用 Hash Map (credentials) 作为用户数据库.
视图 (View Template)
src/main/webapp/login.ftl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html lang="en">
<head>
<title>认证系统</title>
</head>
<body>
<form action="/login?redirect=${RequestParameters.redirect!}" method="POST">
<h2>Login in</h2>
<input type="text" name="username" placeholder="用户名" autofocus="true" />
<input type="text" name="password" placeholder="密码" />
<div>(用户名: hellokoding 密码: hellokoding)</div>
<div style="color: red">${error!}</div>
<br />
<button type="submit">登 录</button>
</form>
</body>
</html>
应用配置
src/main/resources/application.properties1
2spring.freemarker.template-loader-path=/
spring.freemarker.suffix=.ftl
src/main/java/com/example/sso/SsoApplication.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package com.example.sso;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
public class SsoApplication extends SpringBootServletInitializer {
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SsoApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SsoApplication.class, args);
}
}
启动
1 | mvn clean spring-boot:run |
资源系统
项目结构
.
├── pom.xml
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── sso
│ │ ├── SsoApplication.java
│ │ └── auth
│ │ ├── CookieUtil.java
│ │ ├── JwtFilter.java
│ │ ├── JwtUtil.java
│ │ └── ResourceController.java
│ ├── resources
│ │ ├── application.properties
│ └── webapp
│ └── protected-resource.ftl
└── sso.iml
项目依赖
和认证系统 pom.xml 一致, 无须修改.
JwtFilter
JwtFilter 用来使请求强制通过 SSO. 如果 JWT Token 不存在(未认证), 则重定向到认证系统进行认证. 如果 JWT TOKEN 存在(已认证), 则从中提取出用户信息, 并通过请求.
src/main/java/com/example/sso/auth/JwtFilter.java1
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
29package com.example.sso.auth;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends OncePerRequestFilter {
private static final String jwtTokenCookieName = "JWT-TOKEN";
private static final String signingKey = "signingKey";
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String username = JwtUtil.getSubject(httpServletRequest, jwtTokenCookieName, signingKey);
if (username == null) {
String authService = this.getFilterConfig().getInitParameter("services.auth");
httpServletResponse.sendRedirect(authService + "?redirect=" + httpServletRequest.getRequestURL());
} else {
httpServletRequest.setAttribute("username", username);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
}
ResourceController
1 | package com.example.sso.auth; |
视图 (View Template)
src/main/webapp/protected-resource.ftl1
2
3
4
5
6
7
8
9
10
<html lang="en">
<head>
<title>资源系统</title>
</head>
<body>
<h2>你好, ${Request.username!}</h2>
<a href="/logout">登 出</a>
</body>
</html>
应用配置
src/main/resources/application.properties1
2
3
4spring.freemarker.template-loader-path=/
spring.freemarker.suffix=.ftl
services.auth=http://localhost:8080/login
src/main/java/com/example/sso/SsoApplication.java1
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
38package com.example.sso;
import com.example.sso.auth.JwtFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import java.util.Collections;
public class SsoApplication extends SpringBootServletInitializer {
"${services.auth}") (
private String authService;
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new JwtFilter());
registrationBean.setInitParameters(Collections.singletonMap("services.auth", authService));
registrationBean.addUrlPatterns("/protected-resource");
return registrationBean;
}
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SsoApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SsoApplication.class, args);
}
}
启动
启动资源系统 11
mvn clean spring-boot:run -Dserver.port=8180
启动资源系统 21
mvn clean spring-boot:run -Dserver.port=8280
然后大家可以访问 http://localhost:8180
去看看效果哈~~~
主要参考文章: Single Sign On (SSO), Scalable Authentication Example with JSON Web Token (JWT) and Spring Boot