以前已经经过本系列两篇文章《单点登陆系列(1)- OAuth2实施方案 》、《单点登陆系列(2)- Spring OAuth2项目搭建》,跟你们介绍了OAuth2的单点登陆方案,以及如何用Spring搭建OAuth2项目。对于有些朋友来讲,单点登陆的这个概念仍是比较抽象。vue
我想今天这篇文章,配合文中的一个配置OAuth2的Web平台,争取让不会代码的人也能轻松理解OAuth2的单点登陆。包括此次把上篇文章中代码重构了一下,优化了不少功能,本文也会单独作说明。java
网址是:http://kerrysmec.cn:81。作这个web平台的目的,很大程度是想玩玩vue和移动适配。web
平台本次作了三个配置的模块:客户端、登陆页和用户帐号配置。json
回顾一下《单点登陆系列(1)- OAuth2实施方案 》中,每一个第三方平台若是想接入OAuth2的单点登陆,都须要申请client_id和client_secret。那么咱们就将这个第三方平台称为客户端,下面这个界面就是配置新客户端clientId等参数的。segmentfault
介绍一下需求配置客户端的几个参数:浏览器
5.Access Token有效期:由于我配置的是Jwt token,有时间限制,单位为“秒”,如图中access token在7200秒后过时。
6.Refresh Token有效期:同理,259200秒后失效。app
如图中,咱们新增了四行数据,即配置了四个接入单点登陆的客户端,第一个重定向地址为空,后面三个分别是得帆、新浪、腾讯、百度。框架
那么该若是访问客户端接入单点登陆的网址呢?客户端表格中操做列的第一个按钮,点击打开客户端详情弹框,咱们打开baiduClient的弹框,点击“访问”按钮,或者复制单点登陆的访问地址到浏览器就能够啦。ide
不过这时咱们就会发现,不管咱们访问这四个客户端里面的那一个,都会跳转到咱们的登陆页。说明这些第三方的平台已经受咱们单点登陆的控制了,咱们能够试一下,在成功登陆任意一个客户端后,再点击任意平台的“访问”按钮,就能够直接进入对应的重定向网址,而不会再被拦截在登陆页了。(登陆的帐号密码在后文“帐号配置”中设置)svg
你们细心点会发现,在成功登陆进入重定向的页面时,重定向url后面跟了一个参数code。在密码模式里面就能够经过调接口,经过这个code的值,获取包含当前用户帐号信息的access_token和refresh_token,具体细节就参考本系列第一篇文章,不过咱们能够用到上述弹框中的参数 Basic Authoriztion 。
若是客户端中没有配置重定向url,根据前文说的,咱们能够在访问地址中写上任何网址,不受拘束。
我喜欢多姿多彩的登陆页,甚至于像谷歌搜索的主页同样,一年遇到不一样的节日,都有不一样的页面风格切换。
没错,这个登陆页配置功能,就是让你能够实时的切换单点登陆系统的登陆页。在你喜欢的登陆页卡片上,点击“选用”就生效了!我这里偷懒,没有设计不少登陆页,只是弄了几个不一样色系的页面。
既然是单点登陆,用统一的登陆页,那么帐号密码也是惟一入口配置的。在企业里面员工的帐号密码有AD、OID等等来管理。就算存在表里面,也是加密的方式,不会是像我开发的这样明文显示。
可是为了这个平台的功能完整,我仍是加了这个最简单的帐号/密码管理的页面,方便你们自行操做。建立好“用户名”和“密码”,而后就能够在登陆页上成功登陆了。
《单点登陆系列(2)- Spring OAuth2项目搭建》 中对于客户端的配置比较粗糙,是在项目启动时一次性读取全部客户端client的配置信息,放在内存里面实现的。若是有须要加接入新的客户端就须要重启后再生效,咱们能不能把配置信息放在表里面呢?就像咱们上面Web平台作的那样。
Spring OAuth2的官方框架里面就封装了表oauth_client_details,要求只要你按照他的要求建一个一样的表,将读取client的方式换成这种方式,就能够实现动态的从表里面读取client的配置信息。
AuthorizationServerConfigurerAdapter.java
@Resourceprivate DataSource dataSource; @Bean public ClientDetailsService clientDetailsService() { return new JdbcClientDetailsService(dataSource); } @Override public void configure(final ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService()); }
oauth_client_details 表结构
字段名 | 字段描述 | 详细描述 | 范例 |
---|---|---|---|
client_id | 主键,必须惟一,不能为空 | 用于惟一标识每个客户端(client);注册时必须填写(也能够服务端自动生成),这个字段是必须的,实际应用也有叫app_key | OaH1heR2E4eGnBr87Br8FHaUFrA2Q0kE8HqZgpdg8Sw |
resource_ids | 不能为空,用逗号分隔 | 客户端能访问的资源id集合,注册客户端时,根据实际须要可选择资源id,也能够根据不一样的额注册流程,赋予对应的额资源id | order-resource,pay-resource |
client_secret | 必须填写 | 注册填写或者服务端自动生成,实际应用也有叫app_secret, 必需要有前缀表明加密方式 | {bcrypt}![]() |
scope | 不能为空,用逗号分隔 | 指定client的权限范围,好比读写权限,好比移动端仍是web端权限 | read,write / web,mobile |
authorized_grant_types | 不能为空 | 可选值 受权码模式:authorization_code,密码模式:password,刷新token: refresh_token, 隐式模式: implicit: 客户端模式: client_credentials。支持多个用逗号分隔 | password,refresh_token |
web_server_redirect_uri | 可为空 | 客户端重定向uri,authorization_code和implicit须要该值进行校验,注册时填写, | http://baidu.com |
authorities | 可为空 | 指定用户的权限范围,若是受权的过程须要用户登录,该字段不生效,implicit和client_credentials须要 | ROLE_ADMIN,ROLE_USER |
access_token_validity | 可空 | 设置access_token的有效时间(秒),默认(606012,12小时) | 3600 |
refresh_token_validity | 可空 | 设置refresh_token有效期(秒),默认(606024*30, 30填) | 7200 |
additional_information | 可空 | 值必须是json格式 | {"key", "value"} |
autoapprove | false/true/read/write | 默认false,适用于authorization_code模式,设置用户是否自动approval操做,设置true跳过用户确认受权操做页面,直接跳到redirect_uri | false |
第一种方式和框架绑的太死,我推荐用这种方式-基于 ClientDetailsService 接口来实现。我在mongo里面本身建立了一个表,不用要求彻底和第一种方式的表结构同样,只要实现了该接口就行。
OauthClientServiceImpl.java
@Service public class OauthClientServiceImpl implements ClientDetailsService { @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private PaasDao paasDao; @Override public BaseClientDetails loadClientByClientId(String clientId) { OauthClientEO client = paasDao.getClientByClientId(clientId); if (client == null) { throw new ClientRegistrationException(clientId+"无效") ; } BaseClientDetails baseClientDetails=new BaseClientDetails(); baseClientDetails.setClientId(client.getClientId()); baseClientDetails.setClientSecret(EncoderUtil.encryptByBCrypt(client.getClientSecret())); baseClientDetails.setAccessTokenValiditySeconds(client.getAccessTokenValidity()); baseClientDetails.setRefreshTokenValiditySeconds(client.getRefreshTokenValidity()); baseClientDetails.setAuthorizedGrantTypes(Arrays.asList(client.getAuthorizedGrantTypes().split(","))); Collection<SimpleGrantedAuthority> collection = new HashSet<>(); collection.add(new SimpleGrantedAuthority("ROLE_USER")); baseClientDetails.setAuthorities(collection); Set<String> scope = new TreeSet<String>(); scope.add("user_info"); baseClientDetails.setScope(scope); baseClientDetails.setAutoApproveScopes(scope); return baseClientDetails; } }
AuthorizationServerConfigurerAdapter.java
/** * 客户端验证,自定义实现oauthClientService接口 */ @Autowired private OauthClientServiceImpl oauthClientServiceImpl; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(oauthClientServiceImpl).clients(oauthClientServiceImpl); }
和客户端client同样,帐号/密码的校验也是能够经过实现接口来完成的。代码中 PasswordEncoderImpl( ) 是我自定义的加密类,由于企业中密码通常都是加密的,好比base64+md5,这时候就能够自定义去实现这个加密类里面的方法。
UserDetailsServiceImpl.java
@Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PaasDao paasDao; @Override public UserDetails loadUserByUsername(String username) throws BadCredentialsException { //enable :用户已失效 //accountNonExpired:用户账号已过时 //credentialsNonExpired:坏的凭证 //accountNonLocked:用户帐号已锁定 //String password= userFeign.loadUserByUsername(username); UserEO userEO=paasDao.getUserByUsername(username); return new User(username, new PasswordEncoderImpl().encode(userEO.getPassword()), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); }