本文所介绍的项目是一个基于oath2协议的应用,实现的的功能逻辑与QQ互联,微博开放平台类似,都是同一套认证授权流程。
项目结构简单易懂,却不偷工减料,在学习完本文内容后,读者可以直接获取文中的项目代码用于学习或者copy到公司的生产项目中修改后使用,真正达到学以致用的目的。
所涉及技术栈:
- java
- springboot 2.0.1.RELEASE
- spring security 2.0.1.RELEASE
- spring cloud oauth2 2.0.0.M7
- mybatis 2.0.1
本项目包括功能有
- 新用户
- 用户注册自动分配角色权限
- 用户只能访问自己所拥有的角色权限访问路径
- 开放平台
- 用户可以申请获取客户ID和客户密钥
- 用户可以通过客户ID获取授权码
- 用户可以通过客户ID和密钥以及授权码获取access token 和referrsh token和scope
- 资源api服务(order-service/open-api-service)
- 可自定义配置需授权url
- 可自定义配置受限url的访问scope
- 未授权用户或访问权限不足用户,页面提示相信息
- 用户通过access token 来访问对应url
项目概览
首先来看一下项目的结构图,了解项目的大致布局
- .idea:idea工具生成的文件
- docker: 存放Dockerfile文件用于构建容器镜像
- config: 项目中关于spring security和web的配置
- controller:请求控制器
- entity: 对象实体类包
- handler:逻辑处理类包
- mapper:存放mybatis mapper接口类
- service: 业务逻辑处理类
- util: 通用的工具类
- postman:postman工具的测试用例
- sql:项目的初始化sql语句
- templates: 页面模板文件
接下来正式介绍关于项目的细节,由于项目本身就已经有不少中文注释,所以在讲解的时候会收缩起一些代码的具体实现,如果读者不习惯可以在这里点在线比对阅读或者fork到自己的项目里阅读==> 项目代码
用户是如何被拦截认证的
@Description @ date web 安全拦截西置 @ component aEnablewebSecurity public class SecurityConfig @ Autowired private MyAuthenticationFailureHandler failurehandler ; QAutowired private MyAuthenticationSuccessHandter successHandlter ; @ Aautowired extends WebSecurityConfigurerAdapter
- 自定类 SecurityConfig 继承了 WebSecurityConfigurerAdapter,说明SecurityConfig将会是一个 Http安全配置适配器
- 通过
@EnableWebSecurity
打开了 HttpSecurity 的安全配置,则该类将生效
*配置 HttpSecurity 拦截资源 @param http @throws Exception aoverride protected void f configure ( HttpSecurity http ) throws Exception { List < Permission > permissions = permissionMapper . findallPermission (); ExpressionUrLAuthorizationConfigurercHttpSecurity >. ExpressionInterceptUrlRegistry authorizeRequests = http . authorizeRequests (); permissions . forEach ( permission -→ authorizeRequests . antMatchers ( permission . getUrl ()). hasAnyAuthority ( permission . getPermTag ())); authorizeRequests . antMatchers (-. antPattems :“/ login ”).permitAl1() xpressionUrlAuthorizationConfigurer < HttpSecurity >. ExpressioninterceptUrlRegistry . antMatchers (. antPatterns :"/ oauth /**”). permitAll () Expressi sionUrlAuthorizationConfigurer < HttpSecurity >. ExpressioninterceptUrlRegistry . antMatchers (- .-. antPatterns "/ addUser "). permi tAll () Expressic onUrlAuthorizationconfigurer < HttpSecurity >. ExpressionlnterceptUrlRegistry . antMatchers (. antPatterns :"/ register ").permitAl1() ExpresionUrlAuthorizationConfigurer < HttpSecurity >. ExpressionInterceptUrlRegistry . antMatchers (. antPattems :“/**“). fullyAuthenticated () and (). formLogin (). loginPage (“/ login ") FomLoginconfigurer < Htpsecurity > . successHandler ( successHandler ). failureHandler ( failureHandler ) FormLoginConfigurer < HttpSecurity > . and (). csrf (). disable ();
- 重写 WebSecurityConfigurerAdapter 的 configure(HttpSecurity http) 方法
- 指定哪里请求url是不需要拦截直接放行的
- 指定哪些请求是需要拦截校验的
private Passwordtncoder passwordtncoder ; /**.用户认证信息....../. @0verride protected void configure ( AuthenticationManagerBuilderd auth ) t thrOws ExCeptiOn f auth . userDetailsservice ( myUserDetailsService ). passwordEncoder ( new PasswordEncoder (){ @OVerrlde publi String encode ( CharSequence rawPassword ){ return passwordEncoder . encode ( rawPassword ); @Override public boolean matches ( CharSequence rawPassword , String encodedPassword )[ return passwordEncoder . matches ( rawPassword , encodedPassword );
- 重写 WebSecurityConfigurerAdapter 的 configure(AuthenticationManagerBuilder auth) 方法
- 指定处理认证的service服务类为 MyUserDetailsService
- 比对输入的密码和数据库中的用户密码是否一致
import java . util . List ; import java . util . stream . Collectors ;/* k . Qauthor . jianspeng ....*/ acomponent @slf4j public class MvuserDetailsservice QAutowired private userMapper userMapperi i aoverride public UserDetails loadUserByUsername ( String username ) /1.根据教据库蓝闻,用户是否登陆 User user = userMapper . findByUsername ( username );/2.查询该用户信息权限 if ( user nul7)[ /设置用户视限 List < Permission > permissions =. userMapper . findPermissionByUsername ( username ); tog.info("用户信息权:(,权:”, username , permissions . tostring ()); e implements UserDetailsService if (! CollectionUtils . isEmpty permissions )) ListerantedAuthority > simpleGrantedAuthorities = user . setAuthorities ( simpleGrantedAuthorities ); permissions . stream (). map ( permission - new SimpleGrantedAuthority permission . getPermTag ()). collect ( collectors . tolist ()) return user;
- 自定类 MyUserDetailsService 实现了 spring security的UserDetailsService 接口
- 重写 loadUserByUsername(String username) 方法, 其中username是页面传的属性和值,属性是固定的
- 获取用户权限表对应的权限详情,并把内容设置到 UserDetails.Authorities属性中
- 返回 UserDetails的子类 User
用户注册自动分配角色权限
用户注册则往用户表插入数据,同时往用户角色表也插入一份数据。
当然也可以设计的更复杂些,比如根据来源、时间、白名单、内部推荐等设置不同的权限,读者可自行扩展。
@param user user @return String @ PostMapping ("/ addUser ")@ ResponseBody public String addUser ( User user ){ user . setId ( userMapper . getMaxId ()+1); user . setPassword ( passwordEncoder . encode ( user . getPassword ())); user . setCreateDate ( new Date ()); user . setEnabled ( true ); user . setAccountNonExpired ( true ); user . setAccountNonLocked ( true ); user . setCredentialsNonExpired ( true ); userMapper . insert ( user ); userRoleMapper . insert ( user . getId (), roleld :1); return "注册用户成功";
用户只能访问自己所拥有的角色权限访问路径
import java . util . List ; import java . util . stream . Collectors ;/**.@ author . jiangpeng ......*/ acomponent @SLf4J public class MyuserDetailsService implements UserDetailsService { QAutowired private UserMapper userMapperi @0verride public UserDetails loadUserByUsername ( String username ) throws UsernameNotFoundExcepti0n //1.根据数据库查询,用户是否登陆 User user = userMapper . findByUsername ( username ); //2.询该用户信息权限 if ( user 丰 null ){ List < Permission > permissions = userMapper . findPermissionByUsername ( username ); Log . info ("用户信息权限:,权限:仔", username , permissions . tostring ()); if (! CollectioUtils . isEmptypermissions ))( List < GrantedAuthority ? simpleGrantedAuthorities = permissions . stream (). map ( permission - new SimpleGrantedAuthority permission . getPermTag ())). collect ( collectors . toList () user . setAuthorities ( simpleGrantedAuthorities ); return user;
- 取出当前用户的所有权限
- 取出权限的权限标识字段,并包装成一个List 集合
- 保存包装的权限list集合到 UserDetails 对象中
*配置 Httpsecurity 栏截资源 aparam http @ throws Exception Override protected void configure ( HttpSecurity http ) throws Exception i ListcPermission > permissions = permissionMapper . findAllPermission (); ExpressionUrlAuthorizationConfigurercHttpSecurity >. ExpressionInterceptUrlRegistry authorizeRequests = http . authorizeRequests (); l permissions . forEach ( permission -> authorizeRequests . antMatchers ( permission . getUrl ()). hasAnyAuthority ( permission . getPermTag ())); authorizeRequests . antMatchers (.. antPatterns :“/ login ").permitAl1() ExpressionUrlAuthorizationConfigurer < HttpSecurity >. Expressioninte . antMatchers (. antPatterns :“/ oauth /**"). permitAll () ExpressionUrlAuthorizationConfigurer < HtpSecurity >. Expressioninrpttrg
- 取出所有权限表中内容
- http.authorizeRequests()获取当前的认证对象
- 把权限表中的内容全部设置到 authorizeRequests中, antMatchers表示拦截的url, hasAnyAuthority表示可以访问的权限标识
- 因为在上图中user已经设置了自己所拥有权限的权限标识,所以可以访问被拦截的url
用户可以申请获取客户ID和客户密钥
首先来看一下表结构,oauth_client_details 为spring cloud oath2自带的表, user_client_secret 为我们自己创建的表
cfent 1 client _426 client _770 client _92 ( Null ) ( Null ) ( Null ) ( Null ) client _ secret $2a$10$7YJO.unJklmfILm5fV4kLugKPTZZjm/Zshc3HtBwoLsGoggcD2$2a$10$4cjnVXy3KZP.WJm646cmZu9o7JMkMMW5aay2FbI1kXiW56$2a$10$OMBqs3ilrnCG1LbNPBM9nexvbBo5VOWY2LVahSt3crBLrbt$2a$10$WkQsDlNu9IFck8AarbGII.48NrmgX6i3Bh/HWm7kqnJ1UAE
*申请获取客户 ID 和客户密钥 aretUrn Str1ng asetwapping ("/ registerClientId ")@ ResponseBody public String registerClientId (){ User principal =( User ) SecurityContextHolder . getContext (). getAuthentication (). getPrincipal (); if ( principal = null ){ throw new NullPointerException ("用户未登录"); Random random = new Random (); OauthClientDetails clientDetails = new OauthclientDetails (); clientDetails . setClientId (" client _"+ random . nextInt ( bound :999)); clientDetails . setClientSecret ( passwordEncoder . encode ( rawPassword :“123456")); clientDetails . setscope (" all "); clientDetails . setAuthorizedGrantTypes (" password , client _ credentials , refresh _ token , authorization _ code "); clientDetails . setWebserverRedirectUri (“http://localhost:8080/code”); clientDetails . setAuthorities (" ROLE _ ADMIN , ROLE _ USER "); clientDetails . setAccessTokenValidity (7200); clientDetails . setRefreshTokenValidity (7200); oauthclientDetailsMapper . insert ( clientDetails ); UserclientSecret userclientSecret = new UserClientSecret ( principal . getId (), clientDetails . getclientId ()); userClientSecretMapper . save ( userClientsecret ); return "申请客户 ID 和客户密钥成功";
- 生成 OauthClientDetails 数据并保存至数据库中
- 获取当前登录人信息,并绑定OauthClientDetails的clientId至user_client_secret表中
用户可以通过客户ID获取授权码
具体的实现在spring-security-oauth包中的, 非本项目内的自我实现
客户ID获取授权码 请求url:
http://localhost:8080/oauth/authorize?response_type=code&client_id=client_92&redirect_uri=http://localhost:8080/code
- 因为是要获取授权码,response_type=code为固定值
- 客户申请的客户ID
- oauth_client_details 表中的 web_server_redirect_uri
用授权码获取access_token
具体的实现在spring-security-oauth包中的
org.springframework.security.oauth2.provider.endpoint.TokenEndpoint类
,有兴趣的同学可以在里面进行debug调试
用授权码获取access_token 请求url:
http://localhost:8080/oauth/token?grant_type=authorization_code&code=8fGtOV&client_id=client_92&client_secret=123456&redirect_uri=http://localhost:8080/code&scope=all
- 通过客户ID获取到的授权码
- 客户申请的客户ID
- 客户申请的客户ID配套的客户秘钥
- oauth_client_details 表中的 web_server_redirect_uri
- oauth_client_details 表中的 scope
access token:"bfcc2788-1652-4ff4-bcc7-058a8c0de3b6" token_type : "bearer" refresh token:"4af56824-afd4-46e4-8474-f6acf7329660" expires_in:7199 scope:"all"
# 出现如下类似错误标识code失效,重新在获取授权码操作即可 { "error": "invalid_grant", "error_description": "Invalid authorization code: iq30f9" }
商户id和商户秘钥获取accessToken和刷新accessToken
"access_token":"ae502865-8a96-4ad4-9d28-e8ed35bbdecc","token_type": "bearer", "refreshtoken":"96600e32-08c7-4bc6-b2e0-ec05502a3eed""expires in": 7199, "scope":"all"
- grant_type: 授权类型,这里设置为密码模式
- username: 用户名
- password: 用户密码
- client_id: 申请得到的客户Id
- client_secret: 申请得到的客户秘钥
- scope:范围标识,取自 oauth_client_details 表中的 scope
刷新 accessToken
刷新 accessToken的 请求url:
http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=4741d043-e202-4de0-ae21-4f5c7ec5626e&client_id=client_1&client_secret=123456
验证accessToken是否有效
验证accessToken的 请求url:
http://localhost:8080/oauth/check_token?token=1131809b-ee12-4aea-9823-ea4454b96f2d
{ "active": true,"exp":1573730171 "user name": "admin". "authorities":「 "addOrder","showOrder", "update0rder""delete0rder" "client_id":"client_1", "scope": [ "all"
资源api服务
到了这里,我们来看另一个项目 order-service, 其实取名叫 order-api-resource 比较合适,不过也懒得改了。这个项目的结构与代码比较稀少,所以学习起来更为轻松。
如何自定义配置需拦截授权的url
package com.mty.jpsite.configi import … @author jiangpeng*资源服务器处理逻辑配置@date 2019/10/1416:48白 */ -@Configuration ..开启资源服务器配责@EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override @ public void configure(HttpSecurity http) throws Exception { // 对 api/order 等自定义请求进行拦截 http.authorizeRequests().antMatchers(…antPatterns: "/api/order/**").access( attribute: "#oauth2.hasScope('read order //拦截对 /api/product/的 get 请求 .antMatchers(HttpMethod.GET,…antPatterns:"/api/product/**").authenticated()// 拦截对 /api/trade/的 post 请求 .antMatchers(HttpMethod.POST,…antPatterns:"/api/trade/**").authenticated();
通过 antMatchers()
和 authenticated()
方法来配置,查看图中红框内的内容, 这些都是代表需要拦截验证的请求路径url,除了拦截请求外,还可以指定拦截请求方式,比如访问 /api/trade/ ,只对 POST 访问做拦截, GET 请求的访问一律放行。
如何自定义配置受限url的访问scope
@Override public void configure(HttpSecurity http) throws Exception { // 对 api/order 等自定义请求进行拦截 http.authorizeRequests().antMatchers( .antPatterns: "/api/order/** .access( attribute: "#oauth2.hasScope('read_order')") //拦截对 /api/product/的 get 请求 .antMatchers(HttpMethod.GET,…antPatterns:"/api/product/**").authenticated()//拦截对 /api/trade/的 post 请求 .antMatchers(HttpMethod.POST,…antPatterns:"/api/trade/**").authenticated();
通过 access()
方法指定访问的该 url 需要的 scope,取值为 oauth_client_details 表中的 scope 值, 如果scope值与代码中定义的不一致,则会出现如下错误:
"error": "insufficient_scope", "error_description": "Insufficient scope for this resource", "scope": "read_order"
未授权用户或访问权限不足用户,页面提示相应信息
用户如何通过 access token 来访问对应url
level: org.springframework.security: DEBUG security: oauth2: resource: # 从认证授权中心上验证token token-info-uri: http://localhost:8080/oauth/check_token prefer-token-info: true client: access-token-uri: http://localhost:8080/oauth/token user-authorization-uri: http://localhost:8080/oauth/authorize ###appid clientId:client 92 ###appSecret clientSecret:123456
- 配置验证 accessToken 是否有效的服务地址,其实就是上面发放 accessToken 的服务
- 需要配置商户id和商户秘钥获取accessToken,用来通过的开发平台的校验。有的小伙伴可能奇怪了,文中上面也有介绍验证accessToken是否有效,但是并没有需要验证登录,这里为什么要呢? 我们来看下源码:
spring-security-oauth2-2.2.1.RELEASE-sources.jar 中的 RemoteTokenServices.java 用于远程调用 token 服务(开发平台)进行操作
@Override public OAuth2AuthenticationloadAuthentication(StringaccessToken)throws AuthenticationException,InvalidTokenException MultiValueMap<String,String> formData = new LinkedMultiValueMap<<(); formData.add(tokenName,accessToken); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization",getAuthorizationHeader(clientId, clientSecret)); 1 Map<String,Object>map=postForMap(checkTokenEndpointUrl, formData, headers); if(map.containsKey("error")){ logger.debug("check_token returned error: "+ map.get("error")); throw new InvalidTokenException(accessToken);
- 因为给http header 设置了Authorization属性表示需要验证,所以取了上述配置中的 clientId 和 clientSecret,否则发送请求会被开放平台验证失败
- postForMap() 包装参数并执行请求发送