桥接模式-SSO单点登录
- 背景:(业务细节已脱敏)
- 需求:
- 问题:
- 解决方式:
- OAuth2.0 实现单点登录
- 四种授权模式
- 桥接模式优化
- 问题
- 代码实现
背景:(业务细节已脱敏)
基于实习期间的一个代码重构的思考——业务细节已脱敏
基于内部旧框架实现业务toB管理系统,需要迁移数据并新的内部技术框架对进行代码重构
需求:
登录接口是旧项目已有的,包括原有系统的账号密码,邮箱,短信,以及第三方客户OA系统等方式进行授权,现需要对旧框架实现的登录功能迁移到中台进行统一集成
问题:
-
原有旧框架独立于中台,旧框住不再维护,需要迁移
-
业务有所变更,并且时常有变更且仍与k讨论,带有不确定性
-
原有登录方式都写到service类中,函数方法全部堆积在service,代码数量较大,结构混乱
解决方式:
通过桥接模式对登录方式的结构进行整理,完全符合开闭原则,提高代码的扩展性,应对仍在商讨的业务
Autho2详解文章
OAuth2.0 实现单点登录
- 什么是OAuth2.0
OAuth2.0 来实现第三方授权,基于第三方应用访问用户信息的权限(本质上就是给别人调用自己服务接口的权限)。
四种授权模式
- 客户端模式
这是最简单的一种模式,我们可以直接向验证服务器请求一个 Token(这里可能有些小伙伴对Token的概念不是很熟悉,Token 相当于是一个令牌,我们需要在验证服务器 (User Account And Authentication) 服务拿到令牌之后,才能去访问资源,比如用户信息、借阅信息等,这样资源服务器才能知道我们是谁以及是否成功登录了)
- 虽然这种模式比较简便,但是已经失去了用户验证的意义,
- 压根就不是给用户校验准备的,而是更适用于服务内部调用的场景
- 密码模式
密码模式相比客户端模式,就多了用户名和密码的信息,用户需要提供对应账号的用户名和密码,才能获取到 Token
- 虽然这样看起来比较合理,但是会直接将账号和密码泄露给客户端,
- 需要后台完全信任客户端不会拿账号密码去干其他坏事,所以这也不是我们常见的
- 隐式授权模式
首先用户访问页面时,会重定向到认证服务器,接着认证服务器给用户一个认证页面,等待用户授权,用户填写信息完成授权后,认证服务器返回 Token
它适用于没有服务端的第三方应用页面,并且相比前面一种形式,验证都是在验证服务器进行的,敏感信息不会轻易泄露,但是 Token 依然存在泄露的风险 。
- 授权码模式
这种模式是最安全的一种模式,也是推荐使用的一种,比如我们手机上的很多 App 都是使用的这种模式。
相比隐式授权模式,它并不会直接返回 Token,而是返回授权码,真正的 Token 是通过应用服务器访问验证服务器获得的。在一开始的时候,应用服务器(客户端通过访问自己的应用服务器来进而访问其他服务)和验证服务器之间会共享一个 secret,这个东西没有其他人知道,而验证服务器在用户验证完成之后,会返回一个授权码,应用服务器最后将授权码和 secret 一起交给验证服务器进行验证,并且 Token 也是在服务端之间传递,不会直接给到客户端
- 这样就算有人中途窃取了授权码,也毫无意义,因为,Token 的获取必须同时携带授权码和 secret ,
- 但是 secret 第三方是无法得知的,并且 Token 不会直接丢给客户端,大大减少了泄露的风险。
安全性高的原因
- 是在应用服务器上进行验证,不会返回token给前端
为什么先获取到code,再申请取token
- 要换取access_token的三要素:用户的同意授权+第三方appid+第三方app_secret
- 用户输入验证信息后拿到code,说明用户同意授权,但是用户拿不到服务器的第三方appid+第三方app_secret
- 服务器将返回的code,同第三方appid和app_secret一起发送,才能满足三要素拿到真正的token
桥接模式优化
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化 。
问题
- 为什么不直接在service里面直接每种登录方式写一个方法,何必像桥接模式每种登录方式写一个实现类
- 在service里面给每种登录写一个实现方法,我们需要滑动整个文件才能知道有多少个方法,而桥接模式一个登录方式一个类单独维护,一目了然。
- 如果某种登录方式在service实现非常复杂,像需要Autho2等方式的授权,需要挤下很多的方法,肯定是不利于维护的。
- 为什么不用策略模式
- 我觉得桥接模式中包含了策略模式,比如第三方登录的就可以根据不同的需要,选择不同的登录策略,引入桥接模式可以针对登录这一模块添加跟登录相关的其他接口,比如注册,退出登录等接口,注册的话也可以选择不同策略。另外像支付一样比较单一功能的接口一般使用策略模式就足够。 桥接模型更像: 多个接口x多个实现类, 策略模式更像:一个接口类x多实现类。
- 此桥接模式的使用,还根据情况使用了单例模式和工厂模式
- 桥接模式有什么好处
完全开闭原则,新增方法无需修改原代码
类结构清晰,不会全部接口积压在一个service中
代码实现
完整结构图
但是下面的代码对该图的一些瑕疵进行了一下优化
- 瑕疵:上图中的右侧子实现类必须实现右侧接口的全部方法
- 通过在接口层和实现层两者中间引入 抽象层 来解决这个问题
- 瑕疵:每次使用右侧子实现类都要new出来吗?
- 通过工厂模式+单例模式实现右侧子实现类的单例懒加载
具体代码:
- 右侧登录方式接口层
public interface RegisterLoginFuncInterface {
public String login(String account, String password);
public String register(UserInfo userInfo);
public boolean checkUserExists(String userName);
public String login3rd(HttpServletRequest request);
}
- 右侧登录方式的抽象层
public abstract class AbstractRegisterLoginFunc implements RegisterLoginFuncInterface {
protected String commonLogin(String account, String password, UserRepository userRepository) {
UserInfo userInfo = userRepository.findByUserNameAndUserPassword(account, password);
if(userInfo == null) {
return "account / password ERROR!";
}
return "Login Success";
}
protected String commonRegister(UserInfo userInfo, UserRepository userRepository) {
if(commonCheckUserExists(userInfo.getUserName(), userRepository)) {
throw new RuntimeException("User already registered.");
}
userInfo.setCreateDate(new Date());
userRepository.save(userInfo);
return "Register Success!";
}
protected boolean commonCheckUserExists(String userName, UserRepository userRepository) {
UserInfo user = userRepository.findByUserName(userName);
if(user == null) {
return false;
}
return true;
}
public String login(String account, String password) {
throw new UnsupportedOperationException();
}
public String register(UserInfo userInfo){
throw new UnsupportedOperationException();
}
public boolean checkUserExists(String userName){
throw new UnsupportedOperationException();
}
public String login3rd(HttpServletRequest request) {
throw new UnsupportedOperationException();
}
}
- 右侧具体登录方式的实现类
@Component
public class RegisterLoginByDefault extends AbstractRegisterLoginFunc implements RegisterLoginFuncInterface {
@Autowired
private UserRepository userRepository;
@PostConstruct
private void initFuncMap() {
RegisterLoginComponentFactory.funcMap.put("Default", this);
}
@Override
public String login(String account, String password) {
return super.commonLogin(account, password, userRepository);
}
@Override
public String register(UserInfo userInfo) {
return super.commonRegister(userInfo, userRepository);
}
@Override
public boolean checkUserExists(String userName) {
return super.commonCheckUserExists(userName, userRepository);
}
}
Gitee只是第三方登录的一种,其余微信,支付宝等第三方登录可以按需要无缝接入,这里只写一个Gitee作为案例
@Component
public class RegisterLoginByGitee extends AbstractRegisterLoginFunc implements RegisterLoginFuncInterface {
@Value("${gitee.state}")
private String giteeState;
@Value("${gitee.token.url}")
private String giteeTokenUrl;
@Value("${gitee.user.url}")
private String giteeUserUrl;
@Value("${gitee.user.prefix}")
private String giteeUserPrefix;
@Autowired
private UserRepository userRepository;
@PostConstruct
private void initFuncMap() {
RegisterLoginComponentFactory.funcMap.put("GITEE", this);
}
@Override
public String login3rd(HttpServletRequest request) {
String code = request.getParameter("code");
String state = request.getParameter("state");
if(!giteeState.equals(state)) {
throw new UnsupportedOperationException("Invalid state!");
}
//请求Gitee平台获取token,并携带code
String tokenUrl = giteeTokenUrl.concat(code);
JSONObject tokenResponse = HttpClientUtils.execute(tokenUrl, HttpMethod.POST);
String token = String.valueOf(tokenResponse.get("access_token"));
System.out.println(token);
//请求用户信息,并携带 token
String userUrl = giteeUserUrl.concat(token);
JSONObject userInfoResponse = HttpClientUtils.execute(userUrl, HttpMethod.GET);
//获取用户信息,userName添加前缀 GITEE@, 密码保持与userName一致。讨论过程请参见2.3小节
String userName = giteeUserPrefix.concat(String.valueOf(userInfoResponse.get("name")));
String password = userName;
return autoRegister3rdAndLogin(userName, password);
}
private String autoRegister3rdAndLogin(String userName, String password) {
//如果第三方账号已经登录过,则直接登录
if(super.commonCheckUserExists(userName, userRepository)) {
return super.commonLogin(userName, password, userRepository);
}
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
userInfo.setUserPassword(password);
userInfo.setCreateDate(new Date());
//如果第三方账号是第一次登录,先进行“自动注册”
super.commonRegister(userInfo, userRepository);
//自动注册完成后,进行登录
return super.commonLogin(userName, password, userRepository);
}
}
- 左侧抽象调用入口类
public abstract class AbstractRegisterLoginComponent {
protected RegisterLoginFuncInterface funcInterface;
public AbstractRegisterLoginComponent(RegisterLoginFuncInterface funcInterface) {
validate(funcInterface);
this.funcInterface = funcInterface;
}
protected final void validate(RegisterLoginFuncInterface funcInterface) {
if(!(funcInterface instanceof RegisterLoginFuncInterface)) {
throw new UnsupportedOperationException("Unknown register/login function type!");
}
}
public abstract String login(String username, String password);
public abstract String register(UserInfo userInfo);
public abstract boolean checkUserExists(String userName);
public abstract String login3rd(HttpServletRequest request);
}
- 左侧具体入口子类
public class RegisterLoginComponent extends AbstractRegisterLoginComponent{
public RegisterLoginComponent(RegisterLoginFuncInterface funcInterface) {
super(funcInterface);
}
@Override
public String login(String username, String password) {
return funcInterface.login(username, password);
}
@Override
public String register(UserInfo userInfo) {
return funcInterface.register(userInfo);
}
@Override
public boolean checkUserExists(String userName) {
return funcInterface.checkUserExists(userName);
}
@Override
public String login3rd(HttpServletRequest request) {
return funcInterface.login3rd(request);
}
}
- 工厂模式对实现类进行懒加载
- 子实现类的创建运用了工厂模式+单例模式实现懒加载
public class RegisterLoginComponentFactory {
// 缓存 AbstractRegisterLoginComponent(左路)。根据不同的登录方式进行缓存
public static Map<String, AbstractRegisterLoginComponent> componentMap
= new ConcurrentHashMap<>();
// 缓存不同类型的实现类(右路),如:RegisterLoginByDefault,RegisterLoginByGitee
public static Map<String, RegisterLoginFuncInterface> funcMap
= new ConcurrentHashMap<>();
// 根据不同的登录类型,获取 AbstractRegisterLoginComponent
public static AbstractRegisterLoginComponent getComponent(String type) {
//如果存在,直接返回
AbstractRegisterLoginComponent component = componentMap.get(type);
if(component == null) {
//并发情况下,汲取双重检查锁机制的设计,如果componentMap中没有,则进行创建
synchronized (componentMap) {
component = componentMap.get(type);
if(component == null) {
//根据不同类型的实现类(右路),创建RegisterLoginComponent对象,
//并put到map中缓存起来,以备下次使用。
component = new RegisterLoginComponent(funcMap.get(type));
componentMap.put(type, component);
}
}
}
return component;
}
}
- controller调用
@RestController
@RequestMapping("/bridge")
public class UserBridgeController {
@Autowired
private UserBridgeService userBridgeService;
@PostMapping("/login")
public String login(String account, String password) {
return userBridgeService.login(account, password);
}
@PostMapping("/register")
public String register(@RequestBody UserInfo userInfo) {
return userBridgeService.register(userInfo);
}
@GetMapping("/gitee")
public String gitee(HttpServletRequest request) throws IOException {
return userBridgeService.login3rd(request, "GITEE");
}
}
仅个人思考,有错请指出