摘要:单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影、豆瓣日记......真的会让人崩溃。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。像平时我们登录的QQ,在面板上点击邮箱或者QQ空间,不用登录就自动跳转个人主页,这就是单点登录,即我们登录QQ,相关的QQ产品不用再次登录即可进入。
从技术角度讲单点登录分为:跨子域单点登录、完全跨单点域登录
跨子域单点登陆
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
</dependencies>
新增LoginController控制器,用于跳转登录页,认证通过后生成Token并写入cookie:
@Controller
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");
}
@RequestMapping("/")
public String home(){
return "redirect:/login";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping(value = "login", method = RequestMethod.POST)
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;
}
}
附上生成JWT工具类:
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();
}
}
写入cookie工具类:
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.setDomain(domain);
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;
}
}
由于前端模板引擎基于freemaker,所以记得配置:
server.port=8080
spring.freemarker.template-loader-path: /
spring.freemarker.suffix: .ftl
以及添加freemaker模板:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Authentication Service</title>
</head>
<body>
<form method="POST" action="/login?redirect=${RequestParameters.redirect!}">
<h2>Log in</h2>
<input name="username" type="text" placeholder="Username"
autofocus="true"/>
<input name="password" type="password" placeholder="Password"/>
<div>(try username=hellokoding and password=hellokoding)</div>
<div style="color: red">${error!}</div>
<br/>
<button type="submit">Log In</button>
</form>
</body>
</html>
至此第一个单点登陆项目就完成了,下面来处理第二个单点登陆项目。
创建第二个单点登陆项目sso-jwt-resource并引入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
</dependencies>
添加过滤器,用来从cookie中获取token并校验:
@Component
public class JwtFilter extends OncePerRequestFilter {
private static final String jwtTokenCookieName = "JWT-TOKEN";
private static final String signingKey = "signingKey";
@Override
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);
}
}
}
附上cookie工具类和JWT工具类:
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();
}
}
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.setDomain(domain);
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);
cookie.setDomain("localhost");
httpServletResponse.addCookie(cookie);
}
public static String getValue(HttpServletRequest httpServletRequest, String name) {
Cookie cookie = WebUtils.getCookie(httpServletRequest, name);
return cookie != null ? cookie.getValue() : null;
}
}
添加ResourceController控制器:
@Controller
public class ResourceController {
private static final String jwtTokenCookieName = "JWT-TOKEN";
@RequestMapping("/")
public String home() {
return "redirect:/protected-resource";
}
@RequestMapping("/protected-resource")
public String protectedResource() {
return "protected-resource";
}
@RequestMapping("/logout")
public String logout(HttpServletResponse httpServletResponse) {
CookieUtil.clear(httpServletResponse, jwtTokenCookieName);
return "redirect:/";
}
}
freemaker等配置都不能少:
server.port=8082
spring.freemarker.template-loader-path: /
spring.freemarker.suffix: .ftl
services.auth=http://localhost:8080/login
freemaker模板:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Protected Resource Service</title>
</head>
<body>
<h2>Hello, ${Request.username!}</h2>
<a href="/logout">Logout</a>
</body>
</html>
至此第二个单点登陆项目完成。
完全跨单点域登陆
完全跨域登录,是指单点登陆站点没有共同的父域,例如www.bbs.com,www.news.com这种情况就属于完全跨单点域登陆,域名是不相同的,那么使用现有技术没法实现,只能通过SSO单点登录来实现。
这种完全跨单点域登陆不属于本节内容,后续会有整理并在此处标注链接。