文章目录

  • 1. 什么是单点登录
  • 2. 微服务架构下单点登录的思路
  • 3. 使用 Spring Secuirty Oauth2 实现SSO单点登录
    • ①:建表
    • ②:授权服务器逻辑
    • ③:网关逻辑
  • 4. 接口测试

1. 什么是单点登录

单点登录的英文名叫做:Single Sign On(简称SSO)。在早期系统中,大部分项目都是单体架构,随着互联网的发展以及用户量的提升,为了合理利用资源和降低耦合性,于是把单系统拆分成多个子系统。

单点登录:就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。比如:

  • 用户在访问一个微服务架构的app时,只需要一开始访问某个服务时,登录一次即可!后续访问下单、库存服务都无需再次登录。
  • 再比如阿里系的淘宝和天猫,很明显地我们可以知道这是两个系统,但是你在使用的时候,登录了天猫,淘宝也会自动登录。

2. 微服务架构下单点登录的思路

在微服务架构下,我们可以利用网关gateway结合Spring Secuirty Oauth2来实现单点登录,具体步骤如下:

  1. 用户输入账号、密码后,点击登录,进入网关gateway
  2. 在网关内,采用Outh2协议的 密码模式 请求 Outh2授权服务器,获取access_token
  3. 然后客户端访问服务器资源时,带着access_token再次请求网关
  4. 在网关的全局过滤器内部,请求 Outh2授权服务器校验access_token,如果access_token无效,直接返回异常!
  5. 如果access_token校验通过,则解析access_token得到登录用户
  6. 然后继续在网关内部校验用户权限
    • 如果有该资源的权限,则放行
    • 如果没有权限,则返回无权限异常!

大致流程图如下:

3. 使用 Spring Secuirty Oauth2 实现SSO单点登录

①:建表

由于 Oauth2 需要client_idclient_secret等必要信息,且本次采用数据库的方式去存储,所以需要先建立以下几张Oauth2 所必须的表,建表sql参考GitHub地址:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
建表结果如下:

         另外,用户信息,角色信息,权限信息,以及他们之间的关系都需要建表,也就是常说的五表权限!如下所示:

五表权限sql

CREATE TABLE `tb_permission` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',`name` varchar(64) NOT NULL COMMENT '权限名称',`enname` varchar(64) NOT NULL COMMENT '权限英文名称',`url` varchar(255) NOT NULL COMMENT '授权路径',`description` varchar(200) DEFAULT NULL COMMENT '备注',`created` datetime NOT NULL,`updated` datetime NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='权限表';
insert  into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values
(39,38,'查询订单','orderView','/order/selectOrderInfoByIdAndUsername',NULL,'2019-04-04 15:30:30','2019-04-04 15:30:43'),
(45,44,'查询商品','productView','/product/selectProductInfoById',NULL,'2019-04-06 23:49:39','2019-04-06 23:49:41');CREATE TABLE `tb_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',`name` varchar(64) NOT NULL COMMENT '角色名称',`enname` varchar(64) NOT NULL COMMENT '角色英文名称',`description` varchar(200) DEFAULT NULL COMMENT '备注',`created` datetime NOT NULL,`updated` datetime NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';
insert  into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values
(37,0,'超级管理员','admin',NULL,'2019-04-04 23:22:03','2019-04-04 23:22:05');CREATE TABLE `tb_role_permission` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`role_id` bigint(20) NOT NULL COMMENT '角色 ID',`permission_id` bigint(20) NOT NULL COMMENT '权限 ID',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='角色权限表';
insert  into `tb_role_permission`(`id`,`role_id`,`permission_id`) values
(37,37,37),
(38,37,38),
(39,37,39),
(40,37,40),
(41,37,41),
(42,37,42),
(43,37,44),
(44,37,45),
(45,37,46),
(46,37,47),
(47,37,48);CREATE TABLE `tb_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(64) NOT NULL COMMENT '密码,加密存储',`phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',`email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',`created` datetime NOT NULL,`updated` datetime NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `username` (`username`) USING BTREE,UNIQUE KEY `phone` (`phone`) USING BTREE,UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';
insert  into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values
(37,'fox','$2a$10$9ZhDOBp.sRKat4l14ygu/.LscxrMUcDAfeVOEPiYwbcRkoB09gCmi','158xxxxxxx','xxxxxxx@gmail.com','2019-04-04 23:21:27','2019-04-04 23:21:29');CREATE TABLE `tb_user_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`user_id` bigint(20) NOT NULL COMMENT '用户 ID',`role_id` bigint(20) NOT NULL COMMENT '角色 ID',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
insert  into `tb_user_role`(`id`,`user_id`,`role_id`) values
(37,37,37);

②:授权服务器逻辑

授权服务器作为一个单独的服务,其代码与 outh2的配置和使用 一文中的密码模式极其相似,只不过这里我们采用了数据库去存储client_idclient_secret等必要信息,核心代码如下:

  • spring security 配置

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}//验证账号密码时,outh2需要用到这个bean,这里先注入容器@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//用户信息,自定义一个类实现UserDetailsService即可//也包含了权限信息@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}// security配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().and().authorizeRequests()
    //                .antMatchers("/user/getCurrentUser").permitAll().anyRequest().authenticated().and().csrf().disable();}
    }================== 实现 UserDetailsService 的用户信息类 ==================
    //UserService接口
    public interface UserService extends UserDetailsService {SysUser getByUsername(String username);
    }//UserService实现类
    @Service("userDetailsService")
    public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate PermissionMapper permissionMapper;@Overridepublic SysUser getByUsername(String username) {return userMapper.getByUsername(username);}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//获取登录用户SysUser user = getByUsername(username);List<GrantedAuthority> authorities = new ArrayList<>();if(user!=null){List<SysPermission> permissions = permissionMapper.selectByUserId(user.getId());permissions.forEach(permission -> {if (permission!=null && !StringUtils.isEmpty(permission.getEnname())){GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getUrl());//从数据库获取该用户权限,并添加到security中authorities.add(grantedAuthority);}});//返回登录用户 ,以及该用户所具有的权限!return new User(user.getUsername(),user.getPassword(),authorities);}else {throw new UsernameNotFoundException("用户名不存在");}}
    }
    
  • 授权服务器配置

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate DataSource dataSource;@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate TokenStore tokenStore;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {//password模式// http://localhost:8888/oauth/token?username=fox&password=123456&grant_type=password&client_id=gateway-server&client_secret=123123&scope=read//读取数据库的client_id、client_secret等数据clients.withClientDetails(clientDetails());}@Beanpublic ClientDetailsService clientDetails() {//自动读取已创建的oauth_client_details表return new JdbcClientDetailsService(dataSource);}/*** 密码模式需要outh2授权服务器去校验账号、密码* 账号、密码在整合security时,放在security中了* 所以需要整合security的授权管理器authenticationManagerBean*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManager) //使用密码模式需要配置.tokenStore(tokenStore)  //指定token存储到redis.reuseRefreshTokens(false)  //refresh_token是否重复使用.userDetailsService(userDetailsService) //刷新令牌授权包含对用户信息的检查.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {//允许表单认证security.allowFormAuthenticationForClients()// 配置校验token需要带入clientId 和clientSeret配置.checkTokenAccess("isAuthenticated()");}
    }==================== redis 存储方式 ======================
    @Configuration
    public class RedisConfig {@Autowiredprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic TokenStore tokenStore(){// access_tokenreturn new RedisTokenStore(redisConnectionFactory);}
    }
    

③:网关逻辑

在微服务项目中,所有的请求会先经过网关Gateway,然后经过网关的断言工厂、拦截器增强后才会打到具体的某个服务上。所以可以使用网关作为OAuth2的资源服务器角色,对客户端请求进行权限拦截、令牌解析并转发当前登录用户信息给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2相关机制了。

网关在认证授权体系中主要负责

  • 添加全局token认证过滤器,校验token是否有效!

    • 通过向授权服务器发送http请求去校验:http://auth-server/oauth/check_token。校验成功后返回TokenInfo信息,包括该token所具有的权限、过期时间等等!
  • 添加全局权限认证过滤器,检验当前登录用户是否有权限访问对应资源!
    • 由于每个用户的权限信息,都通过实现 UserDetailsService 的类,在配置security时被保存在Outh2的逻辑中,所以校验时只需要把本次请求的url 和 token校验成功后返回的TokenInfo中的权限信息做比对即可,如果存在,则有权限,反之无权限!

全局token认证过滤器:

@Component
@Order(0)  //设置执行优先级,在 全局权限认证过滤器 之前执行
public class AuthenticationFilter implements GlobalFilter, InitializingBean {@Autowiredprivate RestTemplate restTemplate;private static Set<String> shouldSkipUrl = new LinkedHashSet<>();@Overridepublic void afterPropertiesSet() throws Exception {// 在类被初始化完成时,把不拦截认证的请求放入集合shouldSkipUrl.add("/oauth/token");shouldSkipUrl.add("/oauth/check_token");shouldSkipUrl.add("/user/getCurrentUser");}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//获取request请求String requestPath = exchange.getRequest().getURI().getPath();//如果请求url不需要认证,直接跳过if(shouldSkip(requestPath)) {return chain.filter(exchange);}//获取Authorization请求头String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");//Authorization请求头为空,抛异常if(StringUtils.isEmpty(authHeader)) {throw new RuntimeException("请求头为空");}TokenInfo tokenInfo=null;try {//往授权服务发http请求 /oauth/check_token 并封装返回结果!tokenInfo = getTokenInfo(authHeader);}catch (Exception e) {throw new RuntimeException("校验令牌异常");}// 把返回的tokenInfo类,放进全局过滤器的交换器exchange中,// 后续可以在别的全局过滤器中取出tokenInfo信息!exchange.getAttributes().put("tokenInfo",tokenInfo);return chain.filter(exchange);}private boolean shouldSkip(String reqPath) {for(String skipPath:shouldSkipUrl) {if(reqPath.contains(skipPath)) {return true;}}return false;}private TokenInfo getTokenInfo(String authHeader) {// 往授权服务发请求 /oauth/check_token// 获取token的值String token = StringUtils.substringAfter(authHeader, "bearer ");//组装请求头HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//必须设置 basicAuth为对应的 clienId、 clientSecretheaders.setBasicAuth(MDA.clientId, MDA.clientSecret);MultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("token", token);HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);//往授权服务发http请求 /oauth/check_tokenResponseEntity<TokenInfo> response = restTemplate.exchange(MDA.checkTokenUrl, HttpMethod.POST, entity, TokenInfo.class);//获取响应结果 TokenInforeturn response.getBody();}
}====================== 校验 token 后返回的响应结果TokenInfo ================== @Data
public class TokenInfo {private boolean active;private String client_id;private String[] scope;private String username;private String[] aud;//过期时间private Date exp;//该token的授权信息private String[] authorities;}

全局权限认证过滤器:

//权限认证过滤器
@Component
@Order(1)
public class AuthorizationFilter implements GlobalFilter, InitializingBean {private static Set<String> shouldSkipUrl = new LinkedHashSet<>();@Overridepublic void afterPropertiesSet() throws Exception {// 不拦截认证的请求shouldSkipUrl.add("/oauth/token");shouldSkipUrl.add("/oauth/check_token");shouldSkipUrl.add("/user/getCurrentUser");}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//获取request请求String requestPath = exchange.getRequest().getURI().getPath();//如果请求url不需要认证,直接跳过if(shouldSkip(requestPath)) {return chain.filter(exchange);}//从全局过滤器的交换器exchange中取出 之前放入的tokenInfo信息!TokenInfo tokenInfo = exchange.getAttribute("tokenInfo");if(!tokenInfo.isActive()) {throw new RuntimeException("token过期");}//根据tokenInfo信息,鉴权!hasPremisson(tokenInfo,requestPath);return chain.filter(exchange);}private boolean shouldSkip(String reqPath) {for(String skipPath:shouldSkipUrl) {if(reqPath.contains(skipPath)) {return true;}}return false;}private boolean hasPremisson(TokenInfo tokenInfo,String currentUrl) {boolean hasPremisson = false;//登录用户所拥有的请求url权限集合List<String> premessionList = Arrays.asList(tokenInfo.getAuthorities());//与当前请求url,看是否有对应的访问权限for (String url: premessionList) {if(currentUrl.contains(url)) {hasPremisson = true;break;}}//如果没有,抛异常if(!hasPremisson){throw new RuntimeException("没有权限");}return hasPremisson;}
}

网关ymal配置:

server:port: 8880
spring:application:name: gateway-servercloud:gateway:discovery:locator:lower-case-service-id: trueenabled: trueroutes:# 商品服务- id: product-serveruri: lb://product-serverpredicates:- Path=/product/**# 订单服务- id: order_serveruri: lb://order-serverpredicates:- Path=/order/**# 授权服务- id: auth_serveruri: lb://auth-serverpredicates:- Path=/oauth/**,/user/**nacos:discovery:server-addr: localhost:8848main:allow-bean-definition-overriding: true

至此 Spring Secuirty Oauth2实现SSO单点登录核心逻辑已经配置完毕!

4. 接口测试

如果在网关中没有接入授权中心(注释掉上面配置的两个全局过滤器),那么对token就不会做校验,也不管有没有token,都是可以直接访问成功的!如下所示

如果网关中接入授权中心,那么请求必须携带token,且校验通过,才可以访问成功!

其中授权中心的校验token接口返回如下:

使用Spring Secuirty Oauth2实现SSO单点登录相关推荐

  1. 基于Spring Security + OAuth2 的SSO单点登录(服务端)

    相关技术 spring security: 用于安全控制的权限框架 OAuth2: 用于第三方登录认证授权的协议 JWT:客户端和服务端通信的数据载体 传统登录 登录web系统后将用户信息保存在ses ...

  2. java oauth sso 源码_基于Spring Security Oauth2的SSO单点登录+JWT权限控制实践

    概 述 在前文<基于Spring Security和 JWT的权限系统设计>之中已经讨论过基于 Spring Security和 JWT的权限系统用法和实践,本文则进一步实践一下基于 Sp ...

  3. 前后端分离基于Oauth2的SSO单点登录怎样做?

    一.说明 单点登录顾名思义就是在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统,免除多次登录的烦恼:本文主要介绍跨域间的 前后端分离 项目怎样实现单点登录,并且与 非前后端分离 的差 ...

  4. Spring Cloud云架构 - SSO单点登录之OAuth2.0登录流程(2)

    上一篇是站在巨人的肩膀上去研究OAuth2.0,也是为了快速帮助大家认识OAuth2.0,闲话少说,我根据框架中OAuth2.0的使用总结,画了一个简单的流程图(根据用户名+密码实现OAuth2.0的 ...

  5. Spring Cloud云架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)

    上一篇我根据框架中OAuth2.0的使用总结,画了SSO单点登录之OAuth2.0 登出流程,今天我们看一下根据用户token获取yoghurt信息的流程: /** * 根据token获取用户信息 * ...

  6. 整合spring cloud云架构 - SSO单点登录之OAuth2.0登录流程

    现在我们针对于login做成相关的微服务,解析如下: 请求方式:POST 服务URL: http://localhost:8080/user/login 参数类型:application/json H ...

  7. spring + shiro + cas 实现sso单点登录

    sso-shiro-cas spring下使用shiro+cas配置单点登录,多个系统之间的访问,每次只需要登录一次,项目源码 系统模块说明 cas: 单点登录模块,这里直接拿的是cas的项目改了点样 ...

  8. 整合spring cloud云架构 - SSO单点登录之OAuth2.0登录认证(1)

    之前写了很多关于spring cloud的文章,今天我们对OAuth2.0的整合方式做一下笔记,首先我从网上找了一些关于OAuth2.0的一些基础知识点,帮助大家回顾一下知识点: 一.oauth中的角 ...

  9. Java架构-(十) 整合spring cloud云架构 - SSO单点登录之OAuth2.0登录认证(1)

    之前写了很多关于spring cloud的文章,今天我们对OAuth2.0的整合方式做一下笔记,首先我从网上找了一些关于OAuth2.0的一些基础知识点,帮助大家回顾一下知识点: 一.oauth中的角 ...

最新文章

  1. Python两个内置函数——locals 和globals
  2. XamarinAndroid组件教程设置动画的时长参数
  3. Python编程基础:第十节 while循环While Loops
  4. Linux环境下启动Tomcat太慢
  5. 动态载入树 (ASP+数据库)
  6. 太省事了!高分SCI全套优质模板下载
  7. MS17-010 “永恒之蓝“ 修复方案
  8. ae编程语言as_计算机基础以及编程语言
  9. 高清设计素材|自然纸纹理,重构自然与生活的趣味
  10. 只会编程的程序员没有前途
  11. 中文停用词词表-自然语言处理
  12. 【安卓手机驱动无法安装则无法连接电脑,终极100%解决方法】ADB interfacm与 Andriod安装出现黄色感叹号
  13. 【精彩文章】数学家论数学——数学的本质
  14. 中秋之际献上【中秋快乐】藏头诗
  15. Squid代理服务器基础_wuli大世界_新浪博客
  16. 简单易懂的英语学习思维导图(学习篇)
  17. vue RSA加密算法(jsencrypt)的使用
  18. java 测试网速_java心跳测网速Demo
  19. webman apidoc安装、生成接口文档
  20. 苏宁单挑京东 大苏宁战略猜想

热门文章

  1. 二分实现:查找数组中的峰值元素
  2. SpringBoot测试失败并报错: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration
  3. android 拖动缩放窗口大小,Android小应用----图片的拖动、缩放
  4. draggable禁止拖动_通过 JS 实现简单的拖拽功能并且可以在特定元素上禁止拖拽...
  5. 用得最多的altium版本_83版《神雕》有多猛?收视率破90%!金庸最满意的版本...
  6. PyCharm编辑界面提示
  7. Mongodb删除重复数据
  8. ant+jenkins+testng+selenium集成环境搭建
  9. DevExpress GridView 添加和设置右键菜单
  10. qiniudn.com域名已完全恢复