1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > SpringBoot+OAuth2+JWT实现单点登录SSO完整教程 竟如此简单优雅!

SpringBoot+OAuth2+JWT实现单点登录SSO完整教程 竟如此简单优雅!

时间:2021-08-21 17:09:21

相关推荐

SpringBoot+OAuth2+JWT实现单点登录SSO完整教程 竟如此简单优雅!

作者:狂乱的贵公子

来源:/cjsblog/p/10548022.html

1.前言

技术这东西吧,看别人写的好像很简单似的,到自己去写的时候就各种问题,“一看就会,一做就错”。网上关于实现SSO的文章一大堆,但是当你真的照着写的时候就会发现根本不是那么回事儿,简直让人抓狂,尤其是对于我这样的菜鸟。几经曲折,终于搞定了,决定记录下来,以便后续查看。先来看一下效果

2.准备

2.1. 单点登录

最常见的例子是,我们打开淘宝APP,首页就会有天猫、聚划算等服务的链接,当你点击以后就直接跳过去了,并没有让你再登录一次

下面这个图是我再网上找的,我觉得画得比较明白:

可惜有点儿不清晰,于是我又画了个简版的:

重要的是理解:

SSO服务端和SSO客户端直接是通过授权以后发放Token的形式来访问受保护的资源

相对于浏览器来说,业务系统是服务端,相对于SSO服务端来说,业务系统是客户端

浏览器和业务系统之间通过会话正常访问

不是每次浏览器请求都要去SSO服务端去验证,只要浏览器和它所访问的服务端的会话有效它就可以正常访问

2.2. OAuth2

推荐以下几篇博客

《OAuth 2.0》

《Spring Security对OAuth2的支持》

3.利用OAuth2实现单点登录

接下来,只讲跟本例相关的一些配置,不讲原理,不讲为什么

众所周知,在OAuth2在有授权服务器、资源服务器、客户端这样几个角色,当我们用它来实现SSO的时候是不需要资源服务器这个角色的,有授权服务器和客户端就够了。

授权服务器当然是用来做认证的,客户端就是各个应用系统,我们只需要登录成功后拿到用户信息以及用户所拥有的权限即可

之前我一直认为把那些需要权限控制的资源放到资源服务器里保护起来就可以实现权限控制,其实是我想错了,权限控制还得通过Spring Security或者自定义拦截器来做

3.1. Spring Security 、OAuth2、JWT、SSO

在本例中,一定要分清楚这几个的作用

首先,SSO是一种思想,或者说是一种解决方案,是抽象的,我们要做的就是按照它的这种思想去实现它

其次,OAuth2是用来允许用户授权第三方应用访问他在另一个服务器上的资源的一种协议,它不是用来做单点登录的,但我们可以利用它来实现单点登录。在本例实现SSO的过程中,受保护的资源就是用户的信息(包括,用户的基本信息,以及用户所具有的权限),而我们想要访问这这一资源就需要用户登录并授权,OAuth2服务端负责令牌的发放等操作,这令牌的生成我们采用JWT,也就是说JWT是用来承载用户的Access_Token的

最后,Spring Security是用于安全访问的,这里我们我们用来做访问权限控制

4.认证服务器配置

4.1. Maven依赖

<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="/POM/4.0.0"xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath/><!--lookupparentfromrepository--></parent><groupId>com.cjs.sso</groupId><artifactId>oauth2-sso-auth-server</artifactId><version>0.0.1-SNAPSHOT</version><name>oauth2-sso-auth-server</name><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId><version>2.1.3.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency><dependency><groupId>mons</groupId><artifactId>commons-lang3</artifactId><version>3.8.1</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.56</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

这里面最重要的依赖是:spring-security-oauth2-autoconfigure

4.2. application.yml

spring:datasource:url:jdbc:mysql://localhost:3306/permissionusername:rootpassword:123456driver-class-name:com.mysql.jdbc.Driverjpa:show-sql:truesession:store-type:redisredis:host:127.0.0.1password:123456port:6379server:port:8080

4.3. AuthorizationServerConfig(重要)

packagecom.cjs.sso.config;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.Primary;importorg.springframework.security.core.token.DefaultToken;importorg.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;importorg.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;importorg.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;importorg.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;importorg.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;importorg.springframework.security.oauth2.provider.token.DefaultTokenServices;importorg.springframework.security.oauth2.provider.token.TokenStore;importorg.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;importorg.springframework.security.oauth2.provider.token.store.JwtTokenStore;importjavax.sql.DataSource;/***@authorChengJianSheng*@date-02-11*/@Configuration@EnableAuthorizationServerpublicclassAuthorizationServerConfigextendsAuthorizationServerConfigurerAdapter{@AutowiredprivateDataSourcedataSource;@Overridepublicvoidconfigure(AuthorizationServerSecurityConfigurersecurity)throwsException{security.allowFormAuthenticationForClients();security.tokenKeyAccess("isAuthenticated()");}@Overridepublicvoidconfigure(ClientDetailsServiceConfigurerclients)throwsException{clients.jdbc(dataSource);}@Overridepublicvoidconfigure(AuthorizationServerEndpointsConfigurerendpoints)throwsException{endpoints.accessTokenConverter(jwtAccessTokenConverter());endpoints.tokenStore(jwtTokenStore());//endpoints.tokenServices(defaultTokenServices());}/*@Primary@BeanpublicDefaultTokenServicesdefaultTokenServices(){DefaultTokenServicesdefaultTokenServices=newDefaultTokenServices();defaultTokenServices.setTokenStore(jwtTokenStore());defaultTokenServices.setSupportRefreshToken(true);returndefaultTokenServices;}*/@BeanpublicJwtTokenStorejwtTokenStore(){returnnewJwtTokenStore(jwtAccessTokenConverter());}@BeanpublicJwtAccessTokenConverterjwtAccessTokenConverter(){JwtAccessTokenConverterjwtAccessTokenConverter=newJwtAccessTokenConverter();jwtAccessTokenConverter.setSigningKey("cjs");//SetstheJWTsigningkeyreturnjwtAccessTokenConverter;}}

说明:

别忘了**@EnableAuthorizationServer**

Token存储采用的是JWT

客户端以及登录用户这些配置存储在数据库,为了减少数据库的查询次数,可以从数据库读出来以后再放到内存中

4.4. WebSecurityConfig(重要)

packagecom.cjs.sso.config;importcom.cjs.sso.service.MyUserDetailsService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.builders.WebSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;/***@authorChengJianSheng*@date-02-11*/@Configuration@EnableWebSecuritypublicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateMyUserDetailsServiceuserDetailsService;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException{auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}@Overridepublicvoidconfigure(WebSecurityweb)throwsException{web.ignoring().antMatchers("/assets/**","/css/**","/images/**");}@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http.formLogin().loginPage("/login").and().authorizeRequests().antMatchers("/login").permitAll().anyRequest().authenticated().and().csrf().disable().cors();}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}}

4.5. 自定义登录页面(一般来讲都是要自定义的)

packagecom.cjs.sso.controller;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.GetMapping;/***@authorChengJianSheng*@date-02-12*/@ControllerpublicclassLoginController{@GetMapping("/login")publicStringlogin(){return"login";}@GetMapping("/")publicStringindex(){return"index";}}

自定义登录页面的时候,只需要准备一个登录页面,然后写个Controller令其可以访问到即可,登录页面表单提交的时候method一定要是post,最重要的时候action要跟访问登录页面的url一样

千万记住了,访问登录页面的时候是GET请求,表单提交的时候是POST请求,其它的就不用管了

<!DOCTYPEhtml><htmlxmlns:th=""><head><metacharset="utf-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><title>ElaAdmin-HTML5AdminTemplate</title><metaname="description"content="ElaAdmin-HTML5AdminTemplate"><metaname="viewport"content="width=device-width,initial-scale=1"><linktype="text/css"rel="stylesheet"th:href="@{/assets/css/normalize.css}"><linktype="text/css"rel="stylesheet"th:href="@{/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css}"><linktype="text/css"rel="stylesheet"th:href="@{/assets/css/font-awesome.min.css}"><linktype="text/css"rel="stylesheet"th:href="@{/assets/css/style.css}"></head><bodyclass="bg-dark"><divclass="sufee-logind-flexalign-content-centerflex-wrap"><divclass="container"><divclass="login-content"><divclass="login-logo"><h1style="color:#57bf95;">欢迎来到王者荣耀</h1></div><divclass="login-form"><formth:action="@{/login}"method="post"><divclass="form-group"><label>Username</label><inputtype="text"class="form-control"name="username"placeholder="Username"></div><divclass="form-group"><label>Password</label><inputtype="password"class="form-control"name="password"placeholder="Password"></div><divclass="checkbox"><label><inputtype="checkbox">RememberMe</label><labelclass="pull-right"><ahref="#">ForgottenPassword?</a></label></div><buttontype="submit"class="btnbtn-successbtn-flatm-b-30m-t-30"style="font-size:18px;">登录</button></form></div></div></div></div><scripttype="text/javascript"th:src="@{/assets/js/jquery-2.1.4.min.js}"></script><scripttype="text/javascript"th:src="@{/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js}"></script><scripttype="text/javascript"th:src="@{/assets/js/main.js}"></script></body></html>

4.6. 定义客户端

4.7. 加载用户

登录账户

packagecom.cjs.sso.domain;importlombok.Data;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.userdetails.User;importjava.util.Collection;/***大部分时候直接用User即可不必扩展*@authorChengJianSheng*@date-02-11*/@DatapublicclassMyUserextendsUser{privateIntegerdepartmentId;//举个例子,部门IDprivateStringmobile;//举个例子,假设我们想增加一个字段,这里我们增加一个mobile表示手机号publicMyUser(Stringusername,Stringpassword,Collection<?extendsGrantedAuthority>authorities){super(username,password,authorities);}publicMyUser(Stringusername,Stringpassword,booleanenabled,booleanaccountNonExpired,booleancredentialsNonExpired,booleanaccountNonLocked,Collection<?extendsGrantedAuthority>authorities){super(username,password,enabled,accountNonExpired,credentialsNonExpired,accountNonLocked,authorities);}}

加载登录账户

packagecom.cjs.sso.service;importcom.alibaba.fastjson.JSON;importcom.cjs.sso.domain.MyUser;importcom.cjs.sso.entity.SysPermission;importcom.cjs.sso.entity.SysUser;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.stereotype.Service;importorg.springframework.util.CollectionUtils;importjava.util.ArrayList;importjava.util.List;/***@authorChengJianSheng*@date-02-11*/@Slf4j@ServicepublicclassMyUserDetailsServiceimplementsUserDetailsService{@AutowiredprivatePasswordEncoderpasswordEncoder;@AutowiredprivateUserServiceuserService;@AutowiredprivatePermissionServicepermissionService;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{SysUsersysUser=userService.getByUsername(username);if(null==sysUser){log.warn("用户{}不存在",username);thrownewUsernameNotFoundException(username);}List<SysPermission>permissionList=permissionService.findByUserId(sysUser.getId());List<SimpleGrantedAuthority>authorityList=newArrayList<>();if(!CollectionUtils.isEmpty(permissionList)){for(SysPermissionsysPermission:permissionList){authorityList.add(newSimpleGrantedAuthority(sysPermission.getCode()));}}MyUsermyUser=newMyUser(sysUser.getUsername(),passwordEncoder.encode(sysUser.getPassword()),authorityList);log.info("登录成功!用户:{}",JSON.toJSONString(myUser));returnmyUser;}}

4.8. 验证

img

当我们看到这个界面的时候,表示认证服务器配置完成

5.两个客户端

5.1. Maven依赖

<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="/POM/4.0.0"xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath/><!--lookupparentfromrepository--></parent><groupId>com.cjs.sso</groupId><artifactId>oauth2-sso-client-member</artifactId><version>0.0.1-SNAPSHOT</version><name>oauth2-sso-client-member</name><description>DemoprojectforSpringBoot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId><version>2.1.3.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId><version>3.0.4.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

5.2. application.yml

server:port:8082servlet:context-path:/memberSystemsecurity:oauth2:client:client-id:UserManagementclient-secret:user123access-token-uri:http://localhost:8080/oauth/tokenuser-authorization-uri:http://localhost:8080/oauth/authorizeresource:jwt:key-uri:http://localhost:8080/oauth/token_key

img

这里context-path不要设成/,不然重定向获取code的时候回被拦截

5.3. WebSecurityConfig

packagecom.cjs.example.config;importcom.cjs.example.util.EnvironmentUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.builders.WebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;/***@authorChengJianSheng*@date-03-03*/@EnableOAuth2Sso@ConfigurationpublicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateEnvironmentUtilsenvironmentUtils;@Overridepublicvoidconfigure(WebSecurityweb)throwsException{web.ignoring().antMatchers("/bootstrap/**");}@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{if("local".equals(environmentUtils.getActiveProfile())){http.authorizeRequests().anyRequest().permitAll();}else{http.logout().logoutSuccessUrl("http://localhost:8080/logout").and().authorizeRequests().anyRequest().authenticated().and().csrf().disable();}}}

说明:

最重要的注解是@EnableOAuth2Sso

权限控制这里采用Spring Security方法级别的访问控制,结合Thymeleaf可以很容易做权限控制

顺便多提一句,如果是前后端分离的话,前端需求加载用户的权限,然后判断应该显示那些按钮那些菜单

5.4. MemberController

packagecom.cjs.example.controller;importorg.springframework.security.access.prepost.PreAuthorize;importorg.springframework.security.core.Authentication;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.ResponseBody;importjava.security.Principal;/***@authorChengJianSheng*@date-03-03*/@Controller@RequestMapping("/member")publicclassMemberController{@GetMapping("/list")publicStringlist(){return"member/list";}@GetMapping("/info")@ResponseBodypublicPrincipalinfo(Principalprincipal){returnprincipal;}@GetMapping("/me")@ResponseBodypublicAuthenticationme(Authenticationauthentication){returnauthentication;}@PreAuthorize("hasAuthority('member:save')")@ResponseBody@PostMapping("/add")publicStringadd(){return"add";}@PreAuthorize("hasAuthority('member:detail')")@ResponseBody@GetMapping("/detail")publicStringdetail(){return"detail";}}

5.5. Order项目跟它是一样的

server:port:8083servlet:context-path:/orderSystemsecurity:oauth2:client:client-id:OrderManagementclient-secret:order123access-token-uri:http://localhost:8080/oauth/tokenuser-authorization-uri:http://localhost:8080/oauth/authorizeresource:jwt:key-uri:http://localhost:8080/oauth/token_key

5.6. 关于退出

退出就是清空用于与SSO客户端建立的所有的会话,简单的来说就是使所有端点的Session失效,如果想做得更好的话可以令Token失效,但是由于我们用的JWT,故而撤销Token就不是那么容易,关于这一点,在官网上也有提到:

本例中采用的方式是在退出的时候先退出业务服务器,成功以后再回调认证服务器,但是这样有一个问题,就是需要主动依次调用各个业务服务器的logout

6.工程结构

附上源码:/chengjiansheng/cjs-oauth2-sso-demo.git

7. 演示

8.参考

/cjsblog/p/9174797.html

/cjsblog/p/9184173.html

/cjsblog/p/9230990.html

/cjsblog/p/9277677.html

/fooelliot/article/details/83617941

//09/07/user-authentication-with-jwt/

/lihaoyang/p/8581077.html

/charlypage/p/9383420.html

/content/18/0306/17/16915_734789216.shtml

/chenjianandiyi/article/details/78604376

/spring-security-oauth-jwt

/spring-security-oauth-revoke-tokens

/t/630.html

9.文档

https://projects.spring.io/spring-security-oauth/docs/oauth2.html

https://docs.spring.io/spring-security-oauth2-boot/docs/2.1.3.RELEASE/reference/htmlsingle/

https://docs.spring.io/spring-security-oauth2-boot/docs/2.1.3.RELEASE/

https://docs.spring.io/spring-security-oauth2-boot/docs/

https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/

https://docs.spring.io/spring-boot/docs/

https://docs.spring.io/spring-framework/docs/

https://docs.spring.io/spring-framework/docs/5.1.4.RELEASE/

https://spring.io/guides/tutorials/spring-boot-oauth2/

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#core-services-password-encoding

https://spring.io/projects/spring-cloud-security

https://cloud.spring.io/spring-cloud-security/single/spring-cloud-security.html

https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-security.html

https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html#boot-spring-configuration

Springboot启动扩展点超详细总结,再也不怕面试官问了

List去除重复数据的五种方式,学到了...

SpringBoot操作ES进行各种高级查询(必须收藏)

10w行级别数据的Excel导入优化记录

再见,HttpClient!再见,Okhttp!

使用Docker部署SpringBoot+Vue系统

快试试Java8中的StringJoiner吧,真香!

点击阅读全文前往微服务电商教程

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。