单点登录(Single Sign On),简称SSO,即在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,如登录支付宝后在淘宝或天猫网站购物时,其用户状态自动登录。
本文基于JWT实现一种简单的单点登录方式,可用于小型的Web应用集群。JWT的组成和验证机制可自行查阅了解,本文实现单点登录的方式为,将后端应用集群比作一个工厂,设置统一登录入口代表围墙正大门,不同生产车间代表各个应用,工厂里面所有的门都使用相同的锁,前端应用集群则比作各部门工人,共享一把钥匙。基于SpringBoot+Vue/uni-app框架开发的前后端分离项目具体实现过程如下:
图1 单点登录
1、SpringBoot集成JWT实现token验证
为工厂正大门和不同生产车间入口安装相同的锁,即各应用设置相同的JWT_PRIVATE_KEY
// pom文件中引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
// 生成JWT字符串所需的用户基本信息实体类,可结合项目实际情况变更
package com.xxx.bean.utils;
import ...
public class UserToken implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String userName;
public UserToken() {}
public UserToken(int id, String userName) {
this.id = id;
this.userName = userName;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@Override
public String toString() {
return "UserToken{" +
"id=" + id +
", userName='" + userName + '\'' +
'}';
}
}
// 公共常量类
package com.xxx.constants;
public class CommonConstants {
public final static String CONTEXT_TOKEN="Authorization";
public final static String CONTEXT_USERNAME="contextUserName";
public final static String CONTEXT_USER_ID="contextUserId";
public final static String REQ_URL="reqUrl";
public final static String JWT_PRIVATE_KEY ="fancy";
public final static String RENEWAL_TIME = "renewalTime";
}
// JWT工具类
package com.xxx.util;
import ...
public class JwtUtils {
public static String generateToken(UserToken userToken, int expire) throws Exception {
String token = Jwts.builder()
.setSubject(userToken.getUserName())
.claim(CommonConstants.CONTEXT_USER_ID, userToken.getId())
.claim(CommonConstants.RENEWAL_TIME,new Date(System.currentTimeMillis()+expire/2))
.setExpiration(new Date(System.currentTimeMillis()+expire))
.signWith(SignatureAlgorithm.HS256, CommonConstants.JWT_PRIVATE_KEY)
.compact();
return token;
}
public static Claims verify(String token){
return Jwts.parser()
.setSigningKey(CommonConstants.JWT_PRIVATE_KEY).parseClaimsJws(token)
.getBody();
}
public static UserToken getInfoFromToken(String token) throws Exception {
Claims claims = verify(token);
return new UserToken(Integer.parseInt(claims.get(CommonConstants.CONTEXT_USER_ID).toString()),claims.getSubject());
}
}
2、token验证相关基础配置
// 访问权限拦截器类
package com.xxx.interceptor;
import ...
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 对token验证过程中抛出的各类异常信息进行全局处理
String token = request.getHeader(CommonConstants.CONTEXT_TOKEN);
UserToken userToken = JwtUtils.getInfoFromToken(token);
// // 可结合项目实际情况在此处做相应业务逻辑处理,如保存token解析出用户信息等
// FilterContextHandler.setToken(token);
// FilterContextHandler.setUserName(userToken.getUserName());
// FilterContextHandler.setUserId(""+userToken.getId());
return true;
}
}
// 访问权限配置类
package com.xxx.config;
import ...
@Configuration
public class AuthConfig implements WebMvcConfigurer {
@Bean
public AuthInterceptor authInterceptor() {
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration addInterceptor = registry.addInterceptor(authInterceptor());
// 排除配置
addInterceptor.excludePathPatterns("/index.html");
addInterceptor.excludePathPatterns("/favicon.ico");
addInterceptor.excludePathPatterns("/static/**");
addInterceptor.excludePathPatterns("/v1/open/login**");
// 拦截配置
addInterceptor.addPathPatterns("/**");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 这里之所以多了一"/",是为了解决打war时访问不到问题
registry.addResourceHandler("/**").addResourceLocations("/","classpath:/static/");
}
}
3、统一登录入口
// 统一登录入口,该接口在工厂正大门角色应用中创建即可,可结合项目实际情况变更
package com.xxx.controller;
import ...
@RequestMapping("/v1/open")
@RestController
public class LoginController {
private static Map<String, Object> map = new HashMap<>();
@Autowired
private UserDao userDao;
@PostMapping("/login")
public Result<?> login(@Valid @RequestBody User userParam){
List<User> list = userDao.getByName(userParam.getUserName());
if(list.size() < 1 || list == null){
return Result.error("201","用户不存在!");
}
User user = list.get(0);
//密码未做加密处理
if (null == user || !user.getPassword().equals(userParam.getPassword())) {
return Result.error("202","密码错误!");
}
UserToken userToken = new UserToken(user.getId(),user.getUserName());
String token = "";
int expire = 6*60*60*1000;
try {
token = JwtUtils.generateToken(userToken, expire);
} catch (Exception e) {
e.printStackTrace();
}
map.clear();
map.put("token",token);
map.put("expire", expire);
return Result.success(map,"登录成功!");
}
}
上述后端应用在开发完成后,可通过导出Jar包的形式,结合项目实际运行情况,部署到一台(设置不同端口号)或多台服务器中运行。
4、Vue/uni-app项目部署配置
前端应用集群共享一把钥匙,即将多个项目部署到同一站点下,共用一个本地存储,实现token字符串共享,部署模式如下图所示:
图2 前端应用集群部署模式
站点Nginx配置信息如下:
# 可结合项目实际情况变更
server
{
listen 80;
server_name www.xxx.com 127.0.0.1;
index index.php index.html index.htm default.php default.htm default.html;
root /www/wwwroot/www.xxx.com;
try_files $uri $uri/ /index.html;
# 子应用yy路由配置
location /yy{
alias /www/wwwroot/www.xxx.com/yy;
try_files $uri $uri/ /yy/index.html;
}
# 子应用zz路由配置
location /zz{
alias /www/wwwroot/www.xxx.com/zz;
try_files $uri $uri/ /zz/index.html;
}
#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
#error_page 404/404.html;
#SSL-END
#ERROR-PAGE-START 错误页配置,可以注释、删除或修改
#error_page 404 /404.html;
#error_page 502 /502.html;
#ERROR-PAGE-END
#PHP-INFO-START PHP引用配置,可以注释或修改
include enable-php-80.conf;
#PHP-INFO-END
#REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
include /www/server/panel/vhost/rewrite/www.xxx.com.conf;
#REWRITE-END
#禁止访问的文件或目录
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
{
return 404;
}
#一键申请SSL证书验证目录相关设置
location ~ \.well-known{
allow all;
}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 30d;
error_log /dev/null;
access_log /dev/null;
}
location ~ .*\.(js|css)?$
{
expires 12h;
error_log /dev/null;
access_log /dev/null;
}
access_log /www/wwwlogs/www.xxx.com.log;
error_log /www/wwwlogs/www.xxx.com.error.log;
}
5、总结
此方式仅适用于对安全等级要求较低的小集群应用场景,对于不适用以上方式的互联网应用产品需采用分布式部署或另辟蹊径。因作者技术能力有限,文中不足之处还望各位大佬直抒己见、批评指正,反馈邮箱:yourshare@foxmail.com。