技术,看别人写什么好像很简单。自己写的时候会有各种各样的问题。“看到了,做了就错了。”
图片来自Pexels
网上有很多关于SSO的文章,但是当你真正去关注的时候,你会发现根本不是那么回事,简直让人抓狂,尤其是对于我这样的菜鸟来说。
几经周折,终于尘埃落定,决定录下来做后续。我们先来看看效果:
准备①单点登录
最常见的例子就是我们打开淘宝APP,首页会有天猫、聚划算等服务的链接。点开的时候会直接跳过,不会要求你重新登录。
我在网上找到了下面这张图,我觉得挺清楚的:
可惜有点不清楚,所以我画了个简化版:
理解以下内容很重要:
接下来我只讲一些和这个例子相关的配置,不讲原理和原因。
众所周知,OAuth2有几个角色,比如授权服务器、资源服务器、客户端。当我们使用OAuth 2实现SSO时,不需要资源服务器的角色,有授权服务器和客户端就足够了。
当然授权服务器是用来认证的,客户端是各个应用系统。我们只需要获取用户信息和用户登录成功后拥有的权限。
我一直以为权限控制可以通过保护资源服务器中需要权限控制的资源来实现。实际上,我错了,权限控制必须通过Spring安全或自定义拦截器来完成。
①春安、OAuth2、JWT、SSO
在这种情况下,区分这些功能非常重要:
首先,SSO是一种理念,或者说是一种解决方案,比较抽象。我们要做的就是按照它的想法去实施。
其次,OAuth2是一个协议,用来允许用户授权第三方应用访问他在另一台服务器上的资源。它不是用来单点登录的,但是我们可以用它来实现单点登录。
在这个例子中,在实现SSO的过程中,受保护的资源是用户的信息(包括用户的基本信息及其权限)。
但是,如果我们想要访问这个资源,我们需要用户登录并授权。OAuth2服务器负责颁发令牌和其他操作。我们使用JWT来生成这个令牌,也就是说,使用JWT来承载用户的Access_Token。
最后,Spring Security用于安全访问,这里我们用它来进行访问控制。
认证服务器配置Maven依赖性:
& lt?xml版本= & # 34;1.0"编码= & # 34;UTF-8 & # 34;?& gt & lt;xmlns项目= & # 34;http://maven.apache.org/POM/4.0.0" xmlns:xsi = & # 34;http://www.w3.org/2001/XMLSchema-instance" xsi:schema location = & # 34;http://maven.apache.org/POM/4.0.0·http://maven.apache.org/xsd/maven-4.0.0.xsd" & lt;modelVersion & gt4 . 0 . 0 & lt;/model version & gt; & lt;parent & gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gt弹簧-启动-启动-父母& lt/artifact id & gt; & lt;版本& gt发布& lt/version & gt; & lt;relative path/>;& lt!-从存储库中查找父级-& gt; & lt;/parent & gt; & lt;groupId & gtcom . cjs . SSO & lt;/groupId & gt; & lt;artifactId & gtoauth 2-SSO-auth-server & lt;/artifact id & gt; & lt;版本& gt0 . 0 . 1-快照& lt/version & gt; & lt;名称& gtoauth 2-SSO-auth-server & lt;/name & gt; & lt;属性& gt & lt;java.version & gt1.8 & lt/Java . version & gt; & lt;/properties & gt; & lt;依赖关系& gt & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gtspring-boot-starter-data-JPA & lt;/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gtspring-boot-starter-data-redis & lt;/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gt弹簧启动安全& lt/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . security . oauth . boot & lt;/groupId & gt; & lt;artifactId & gtspring-security-oauth 2-auto configure & lt;/artifact id & gt; & lt;版本& gt发布& lt/version & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gt弹簧靴起动器百里香叶& lt/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gtspring-boot-starter-web & lt;/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . session & lt;/groupId & gt; & lt;artifactId & gtspring-session-data-redis & lt;/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtmysql & lt/groupId & gt; & lt;artifactId & gtMySQL-connector-Java & lt;/artifact id & gt; & lt;范围& gt运行时& lt/scope & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg.projectlombok & lt/groupId & gt; & lt;artifactId & gt龙目岛& lt/artifact id & gt; & lt;可选& gttrue & lt/可选& gt & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gt弹簧启动起动器测试& lt/artifact id & gt; & lt;范围& gt测试& lt/scope & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . security & lt/groupId & gt; & lt;artifactId & gt春天-安全-测试& lt/artifact id & gt; & lt;范围& gt测试& lt/scope & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . Apache . commons & lt;/groupId & gt; & lt;artifactId & gtcommons-lang 3 & lt;/artifact id & gt; & lt;版本& gt3 . 8 . 1 & lt;/version & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtcom .阿里巴巴& lt/groupId & gt; & lt;artifactId & gtfastjson & lt/artifact id & gt; & lt;版本& gt1 . 2 . 56 & lt;/version & gt; & lt;/dependency & gt; & lt;/dependencies & gt; & lt;构建& gt & lt;插件& gt & lt;插件& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gtspring-boot-maven-plugin</artifact id & gt; & lt;/plugin & gt; & lt;/plugins & gt; & lt;/build & gt; & lt;/project & gt;最重要的依赖项是:spring-security-oauth 2-auto configure。
应用程序. yml:
spring:[/H/]data source:[/H/]URL:JDBC:MySQL://localhost:3306/permission[/H/]用户名:root 密码:123456[/H/]driver-class-name:com . MySQL . JDBC . driver[/H/]JPA:[/H/]show-SQL:true[/H/]session:[/H/]store-type:redis[/H/]redis:[/H/]host
包com . cjs . SSO . config; 导入org . spring framework . beans . factory . annotation . auto wired; 导入org . spring framework . context . annotation . bean; import org . spring framework . context . annotation . configuration; 导入org . spring framework . context . annotation . primary; import org . spring framework . security . core . token . default token; import org . spring framework . security . oauth 2 . config . annotation . configurers . clientdetailsserviceconfigurer; import org . spring framework . security . oauth 2 . config . annotation . web . configuration . authorizationserverconfigureradapter; import org . spring framework . security . oauth 2 . config . annotation . web . configuration . enableauthorizationserver; import org . spring framework . security . oauth 2 . config . annotation . web . configurers . authorizationserverendpointsconfigurer; import org . spring framework . security . oauth 2 . config . annotation . web . configurers . authorizationserversecurityconfigurer; import org . spring framework . security . oauth 2 . provider . token . defaulttokenservices; import org . spring framework . security . oauth 2 . provider . token . token store; import org . spring framework . security . oauth 2 . provider . token . store . jwtaccesstokenconverter; import org . spring framework . security . oauth 2 . provider . token . store . jwttokenstore; 导入javax . SQL . data source; /* * * @作者成建生 * @日期2019-02-11 */ @ Configuration @ EnableAuthorizationServer 公共类AuthorizationServerConfig扩展AuthorizationServerConfigurerAdapter { @ Autowired 私有DataSource dataSource @ Override public void configure(AuthorizationServerSecurityConfigurer security)抛出异常{ security . allowformauthenticationforclients(); security . token key access(& # 34;is authenticated()& # 34;); } @ Override public void configure(ClientDetailsServiceConfigurer clients)抛出异常{ clients . JDBC(data source); } @ Override public void configure(AuthorizationServerEndpointsConfigurer endpoints)抛出异常{ endpoints . accesstokenconverter(jwtAccessTokenConverter()); endpoints . token store(jwtTokenStore()); //endpoints . token services(defaultTokenServices()); } /* @ Primary @ Bean public DefaultTokenServices DefaultTokenServices(){ DefaultTokenServices DefaultTokenServices = new DefaultTokenServices(); defaulttokenservices . settokenstore(jwtTokenStore()); defaulttokenservices . setsupportrefreshtoken(true); 返回defaultTokenServices } */ @ Bean public JwtTokenStore JwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } @ Bean public JwtAccessTokenConverter JwtAccessTokenConverter(){ JwtAccessTokenConverter JwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtaccesstokenconverter . set signing key(& # 34;cjs & # 34);//设置JWT签名密钥 返回jwtAccessTokenConverter } }描述:
WebSecurityConfig(重要):
包com . cjs . SSO . config; 导入com . cjs . SSO . service . myuserdetailsservice; import org . spring framework . beans . factory . annotation . auto wired; 导入org . spring framework . context . annotation . bean; import org . spring framework . context . annotation . configuration; import org . spring framework . security . config . annotation . authentic ation . builders . authenticationmanagerbuilder; import org . spring framework . security . config . annotation . web . builders . http security; import org . spring framework . security . config . annotation . web . builders . web security; import org . spring framework . security . config . annotation . web . configuration . enable web security; import org . spring framework . security . config . annotation . web . configuration . websecurityconfigureradapter; import org . spring framework . security . crypto . bcrypt . bcryptpasswordencoder; import org . spring framework . security . crypto . password . password encoder; /* * * @作者成建生 * @日期2019-02-11 */ @配置 @EnableWebSecurity 公共类WebSecurityConfig扩展web security configureradapter { @ Autowired private myuserdetailssservice userDetailsService; @ Override protected void configure(AuthenticationManagerBuilder auth)抛出异常{ auth . userdailsservice(userdailsservice)。password encoder(password encoder()); } @ Override public void configure(web security web)抛出异常{ web . ignitioning()。蚂蚁匹配器(& # 34;/assets/* * & # 34;, "/CSS/* * & # 34;, "/images/* * & # 34;); } @ Override protected void configure(http security http)抛出异常{ http.formLogin() 。log in page(& # 34;/log in & # 34;) 。和() 。authorizeRequests() 。蚂蚁匹配器(& # 34;/log in & # 34;).permitAll() 。anyRequest() 。authenticated() 。和()。csrf()。禁用()。CORS(); } @ Bean public password encoder password encoder(){ return new BCryptPasswordEncoder(); } }自定义登录页面(一般来说需要自定义):
包com . cjs . SSO . controller; 导入org . spring framework . stereotype . controller; import org . spring framework . web . bind . annotation . get mapping; /* * * @作者成建生 * @日期2019-02-12 */ @ Controller 公共类log in Controller { @ get mapping(& # 34;/log in & # 34;) public String log in(){ return & # 34;登录& # 34;; } @ get mapping(& # 34;/") public String index(){ return & # 34;索引& # 34;; } }自定义登录页面时,只需要准备一个登录页面,然后编写一个控制器使其可访问。提交登录页面表单时,方法必须是post,最重要的是,动作应该与访问登录页面的url相同。
请记住,当您访问登录页面时,这是一个GET请求,当提交表单时,这是一个POST请求。其他的不用担心。
& lt!DOCTYPE html & gt & lt;html xmlns:th = & # 34;http://www . thyme leaf . org & # 34;& gt & lt;head & gt & lt;meta charset = & # 34utf-8 & # 34;& gt & lt;meta http-equiv = & # 34;X-UA兼容& # 34;内容= & # 34;IE = edge & # 34& gt & lt;title & gtEla管理- HTML5管理模板& lt/title & gt; & lt;meta name = & # 34描述& # 34;内容= & # 34;Ela管理- HTML5管理模板& # 34;& gt & lt;meta name = & # 34视窗& # 34;内容= & # 34;width =设备宽度,initial-scale = 1 & # 34;& gt & lt;链接类型= & # 34;text/CSS & # 34;rel = & # 34样式表& # 34;th:href = & # 34;@ {/assets/CSS/normalize . CSS } & # 34;& gt & lt;链接类型= & # 34;text/CSS & # 34;rel = & # 34样式表& # 34;th:href = & # 34;@ {/assets/bootstrap-4 . 3 . 1-dist/CSS/bootstrap . min . CSS } & # 34;& gt & lt;链接类型= & # 34;text/CSS & # 34;rel = & # 34样式表& # 34;th:href = & # 34;@ {/assets/CSS/font-awesome . min . CSS } & # 34;& gt & lt;链接类型= & # 34;text/CSS & # 34;rel = & # 34样式表& # 34;th:href = & # 34;@ {/assets/CSS/style . CSS } & # 34;& gt & lt;/head & gt; & lt;body class = & # 34BG-dark & # 34;& gt & lt;div class = & # 34sufee-log in d-flex align-content-center flex-wrap & # 34;& gt & lt;div class = & # 34集装箱& # 34;& gt & lt;div class = & # 34登录-内容& # 34;& gt & lt;div class = & # 34登录标志& # 34;& gt & lt;h1 style = & # 34颜色:# 57bf95"& gt欢迎来到王者荣耀
加载用户,登录帐户:
包com . cjs . SSO . domain; 导入龙目岛。数据; import org . spring framework . security . core . granted authority; 导入org . spring framework . security . core . user details . user; 导入Java . util . collection; /** *很多时候不需要展开 * @作者盛 * @ date 2019-02-11 */ @//比如部门ID 私有字符串移动;//比如我们要添加一个字段,这里我们添加一个mobile来表示手机号码 public my user(字符串用户名,字符串密码,集合
包com . cjs . SSO . service; import com . Alibaba . fast JSON . JSON; 导入com . cjs . SSO . domain . my user; import com . cjs . SSO . entity . sys permission; 导入com . cjs . SSO . entity . sys user; 导入lombok . extern . SLF 4j . SLF 4j; import org . spring framework . beans . factory . annotation . auto wired; import org . spring framework . security . core . authority . simplegrantedauthority; import org . spring framework . security . core . user details . user details; import org . spring framework . security . core . user details . user details service; import org . spring framework . security . core . user details . usernamenotfoundexception; import org . spring framework . security . crypto . password . password encoder; 导入org . spring framework . stereotype . service; 导入org . spring framework . util . collection utils; 导入Java . util . ArrayList; 导入Java . util . list; /* * * @作者成建生 * @日期2019-02-11 */ @ Slf4j @ Service 公共类MyUserDetailsService实现userdailsservice { @ Autowired private password encoder password encoder; @ Autowired private UserService UserService; @ Autowired private permission service permission service; @ Override public user details loaduserbysusername(String username)throws UsernameNotFoundException { sys user sys user = userservice . getbyusername(username); if(null = = sysUser){ log . warn(& # 34;用户{}不存在& # 34;,用户名); throw new UsernameNotFoundException(username); } List & lt;SysPermission & gtpermission list = permission service . findbyuserid(sysuser . getid()); List & lt;SimpleGrantedAuthority & gtauthorityList = new ArrayList & lt& gt(); 如果(!collection utils . isempty(permission list)){ for(sys permission sys permission:permission list){ authority list . add(new SimpleGrantedAuthority(sys permission . getcode()); } } my user my user = new my user(sysuser . get username()、password encode . encode(sysuser . get password())、authority list); log . info(& # 34;登录成功!用户:{ } & # 34;,JSON . tojsonstring(my user)); return my user; } }验证:
当我们看到这个界面时,意味着认证服务器配置完成。
两个客户端Maven依赖性:
& lt?xml版本= & # 34;1.0"编码= & # 34;UTF-8 & # 34;?& gt & lt;xmlns项目= & # 34;http://maven.apache.org/POM/4.0.0" xmlns:xsi = & # 34;http://www.w3.org/2001/XMLSchema-instance" xsi:schema location = & # 34;http://maven.apache.org/POM/4.0.0·http://maven.apache.org/xsd/maven-4.0.0.xsd" & lt;modelVersion & gt4 . 0 . 0 & lt;/model version & gt; & lt;parent & gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gt弹簧-启动-启动-父母& lt/artifact id & gt; & lt;版本& gt发布& lt/version & gt; & lt;relative path/>;& lt!-从存储库中查找父级-& gt; & lt;/parent & gt; & lt;groupId & gtcom . cjs . SSO & lt;/groupId & gt; & lt;artifactId & gtoauth 2-SSO-client-member & lt;/artifact id & gt; & lt;版本& gt0 . 0 . 1-快照& lt/version & gt; & lt;名称& gtoauth 2-SSO-client-member & lt;/name & gt; & lt;描述& gtSpring Boot示范项目& lt/description & gt; & lt;属性& gt & lt;java.version & gt1.8 & lt/Java . version & gt; & lt;/properties & gt; & lt;依赖关系& gt & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gtspring-boot-starter-data-JPA & lt;/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gtspring-boot-starter-oauth 2-client & lt;/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gt弹簧-启动-启动-安全& lt/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . security . oauth . boot & lt;/groupId & gt; & lt;artifactId & gtspring-security-oauth 2-auto configure & lt;/artifact id & gt; & lt;版本& gt发布& lt/version & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gt弹簧靴起动器百里香叶& lt/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . thyme leaf . extras & lt;/groupId & gt; & lt;artifactId & gt百里香叶-extras-spring security 5 & lt;/artifact id & gt; & lt;版本& gt发布& lt/version & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gtspring-boot-starter-web & lt;/artifact id & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtcom.h2database & lt/groupId & gt; & lt;artifactId & gth2 & lt/artifact id & gt; & lt;范围& gt运行时& lt/scope & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg.projectlombok & lt/groupId & gt; & lt;artifactId & gt龙目岛& lt/artifact id & gt; & lt;可选& gttrue & lt/可选& gt & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gt弹簧启动起动器测试& lt/artifact id & gt; & lt;范围& gt测试& lt/scope & gt; & lt;/dependency & gt; & lt;依赖性& gt & lt;groupId & gtorg . spring framework . security & lt/groupId & gt; & lt;artifactId & gt春天-安全-测试& lt/artifact id & gt; & lt;范围& gt测试& lt/scope & gt; & lt;/dependency & gt; & lt;/dependencies & gt; & lt;构建& gt & lt;插件& gt & lt;插件& gt & lt;groupId & gtorg . spring framework . boot & lt;/groupId & gt; & lt;artifactId & gtspring-boot-maven-plugin</artifact id & gt; & lt;/plugin & gt; & lt;/plugins & gt; & lt;/build & gt; & lt;/project & gt;应用程序. yml:
server: port:8082 servlet: context-path:/member system security: oauth 2: client:id:user management client-secret:user 123 access-token-uri:http://localhost:8080/oauth/token user-authorization-uri:http://localhost这里,不要将context-path设置为/,否则当您重定向以获取代码时会被阻止。
WebSecurityConfig:
包com . cjs . example . config; 导入com . cjs . example . util . environment utils; import org . spring framework . beans . factory . annotation . auto wired; import org . spring framework . boot . auto configure . security . oauth 2 . client . enableoauth 2s so; import org . spring framework . context . annotation . configuration; import org . spring framework . security . config . annotation . web . builders . http security; import org . spring framework . security . config . annotation . web . builders . web security; import org . spring framework . security . config . annotation . web . configuration . websecurityconfigureradapter; /* * * @作者成建生 * @日期2019-03-03 */ @ enableoauth 2s so @ Configuration 公共类WebSecurityConfig扩展web security configureradapter { @ Autowired private environment utils environment utils; @ Override public void configure(web security web)抛出异常{ web . ignition()。蚂蚁匹配器(& # 34;/bootstrap/* * & # 34;); } @ Override protected void configure(http security http)抛出异常{ if(& # 34;本地& # 34;。equals(environment utils . getactiveprofile()){ http . authorizerequests()。anyRequest()。permit all(); }else { http.logout()。logoutSuccessUrl(& # 34;http://localhost:8080/logout & # 34;) 。和() 。authorizeRequests() 。anyRequest()。authenticated() 。和() 。csrf()。disable(); } } }描述:
成员控制器:
包com . cjs . example . controller; import org . spring framework . security . access . prepost . pre authorize; 导入org . spring framework . security . core . authentic ation; 导入org . spring framework . stereotype . controller; import org . spring framework . web . bind . annotation . get mapping; import org . spring framework . web . bind . annotation . post mapping; import org . spring framework . web . bind . annotation . request mapping; import org . spring framework . web . bind . annotation . response body; 导入Java . security . principal; /* * * @作者成建生 * @日期2019-03-03 */ @ Controller @ request mapping(& # 34;/member & # 34;) public class member controller { @ get mapping(& # 34;/list & # 34;) public String list(){ return & # 34;成员/列表& # 34;; } @ get mapping(& # 34;/info & # 34;) @ResponseBody 公共委托人信息(委托人委托人){ 返回委托人; } @ get mapping(& # 34;/我& # 34;) @ResponseBody 公共认证me(认证认证){ 返回认证; } @预授权(& # 34;hasAuthority(& # 39;成员:保存& # 39;)") @ response body @ post mapping(& # 34;/add & # 34;) public String add(){ return & # 34;添加& # 34;; } @预授权(& # 34;hasAuthority(& # 39;成员:详细& # 39;)") @ response body @ get mapping(& # 34;/detail & # 34;) public String detail(){ return & # 34;细节& # 34;; } }订单项目保持原样:
server: port:8083 servlet: context-path:/order system security: oauth 2: client:id:order management client-secret:order 123 access-Token-uri:http://localhost:8080/oauth/Token user-authorization-uri:http://localhost:88简单来说,就是让所有端点的会话失效。如果想做得更好,可以让令牌失效。但是因为我们用的是JWT,所以令牌撤销不是那么容易,官网也有提到:
在本例中,采用的方法是在退出时退出业务服务器,成功后再回调认证服务器,但这种方式存在一个问题,即需要主动依次调用注销各个业务服务器。
工程结构附上源代码:
Https://github.com/chengjiansheng/cjs-oauth2-sso-demo.git示威
作者:废物大哥
编辑:陶佳龙
资料来源:cnblogs.com/cjsblog/p/10548022.html
最新评论