内容简介:OAuth2 实现单点登录 SSO
(给ImportNew加星标,提高 Java 技能)
作者:废物大师兄
www.cnblogs.com/cjsblog/p/10548022.html
1. 前言
技术这东西吧,看别人写的好像很简单似的,到自己去写的时候就各种问题,“一看就会,一做就错”。网上关于实现SSO的文章一大堆,但是当你真的照着写的时候就会发现根本不是那么回事儿,简直让人抓狂,尤其是对于我这样的菜鸟。几经曲折,终于搞定了,决定记录下来,以便后续查看。先来看一下效果
2. 准备
2.1. 单点登录
最常见的例子是,我们打开淘宝APP,首页就会有天猫、聚划算等服务的链接,当你点击以后就直接跳过去了,并没有让你再登录一次
下面这个图是我在网上找的,我觉得画得比较明白:
可惜有点儿不清晰,于是我又画了个简版的:
重要的是理解:
SSO服务端和SSO客户端直接是通过授权以后发放Token的形式来访问受保护的资源
相对于浏览器来说,业务系统是服务端,相对于SSO服务端来说,业务系统是客户端
浏览器和业务系统之间通过会话正常访问
不是每次浏览器请求都要去SSO服务端去验证,只要浏览器和它所访问的服务端的会话有效它就可以正常访问
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依赖
"1.0" encoding="UTF-8"?>
"http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
com.cjs.sso
oauth2-sso-auth-server
0.0.1-SNAPSHOT
oauth2-sso-auth-server
1.8
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.3.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.springframework.session
spring-session-data-redis
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
org.apache.commons
commons-lang3
3.8.1
com.alibaba
fastjson
1.2.56
org.springframework.boot
spring-boot-maven-plugin
这里面最重要的依赖是:spring-security-oauth2-autoconfigure
4.2. application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/permission
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
jpa:
show-sql: true
session:
store-type: redis
redis:
host: 127.0.0.1
password: 123456
port: 6379
server:
port: 8080
4.3. AuthorizationServerConfig(重要)
package com.cjs.sso.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.token.DefaultToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import javax.sql.DataSource;
/**
* @author ChengJianSheng
* @date 2019-02-11
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.tokenKeyAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter());
endpoints.tokenStore(jwtTokenStore());
// endpoints.tokenServices(defaultTokenServices());
}
/*@Primary
@Bean
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(jwtTokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}*/
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("cjs"); // Sets the JWT signing key
return jwtAccessTokenConverter;
}
}
说明:
别忘了@EnableAuthorizationServer
Token存储采用的是JWT
客户端以及登录用户这些配置存储在数据库,为了减少数据库的查询次数,可以从数据库读出来以后再放到内存中
4.4. WebSecurityConfig(重要)
package com.cjs.sso.config;
import com.cjs.sso.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author ChengJianSheng
* @date 2019-02-11
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable().cors();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.5. 自定义登录页面(一般来讲都是要自定义的)
package com.cjs.sso.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author ChengJianSheng
* @date 2019-02-12
*/
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/")
public String index() {
return "index";
}
}
自定义登录页面的时候,只需要准备一个登录页面,然后写个Controller令其可以访问到即可,登录页面表单提交的时候method一定要是post,最重要的时候action要跟访问登录页面的url一样
千万记住了,访问登录页面的时候是GET请求,表单提交的时候是POST请求,其它的就不用管了
"http://www.thymeleaf.org">
"utf-8">
"X-UA-Compatible" content="IE=edge">
Ela Admin - HTML5 Admin Template
"description" content="Ela Admin - HTML5 Admin Template">
"viewport" content="width=device-width, initial-scale=1">
"text/css" rel="stylesheet" th:href="@{/assets/css/normalize.css}">
"text/css" rel="stylesheet" th:href="@{/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css}">
"text/css" rel="stylesheet" th:href="@{/assets/css/font-awesome.min.css}">
"text/css" rel="stylesheet" th:href="@{/assets/css/style.css}">
class="bg-dark">
class="sufee-login d-flex align-content-center flex-wrap">
class="container">
class="login-content">
class="login-logo">
"color: #57bf95;">欢迎来到王者荣耀
class="login-form">
4.6. 定义客户端
4.7. 加载用户
登录账户
package com.cjs.sso.domain;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
* 大部分时候直接用User即可不必扩展
* @author ChengJianSheng
* @date 2019-02-11
*/
@Data
public class MyUser extends User {
private Integer departmentId; // 举个例子,部门ID
private String mobile; // 举个例子,假设我们想增加一个字段,这里我们增加一个mobile表示手机号
public MyUser(String username, String password, Collection extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
加载登录账户
package com.cjs.sso.service;
import com.alibaba.fastjson.JSON;
import com.cjs.sso.domain.MyUser;
import com.cjs.sso.entity.SysPermission;
import com.cjs.sso.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @author ChengJianSheng
* @date 2019-02-11
*/
@Slf4j
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userService.getByUsername(username);
if (null == sysUser) {
log.warn("用户{}不存在", username);
throw new UsernameNotFoundException(username);
}
List permissionList = permissionService.findByUserId(sysUser.getId());
List authorityList = new ArrayList<>();
if (!CollectionUtils.isEmpty(permissionList)) {
for (SysPermission sysPermission : permissionList) {
authorityList.add(new SimpleGrantedAuthority(sysPermission.getCode()));
}
}
MyUser myUser = new MyUser(sysUser.getUsername(), passwordEncoder.encode(sysUser.getPassword()), authorityList);
log.info("登录成功!用户: {}", JSON.toJSONString(myUser));
return myUser;
}
}
4.8. 验证
当我们看到这个界面的时候,表示认证服务器配置完成
5. 两个客户端
5.1. Maven依赖
"1.0" encoding="UTF-8"?>
"http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
com.cjs.sso
oauth2-sso-client-member
0.0.1-SNAPSHOT
oauth2-sso-client-member
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-oauth2-client
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.3.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
org.thymeleaf.extras
thymeleaf-extras-springsecurity5
3.0.4.RELEASE
org.springframework.boot
spring-boot-starter-web
com.h2database
h2
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
org.springframework.boot
spring-boot-maven-plugin
5.2. application.yml
server:
port: 8082
servlet:
context-path: /memberSystem
security:
oauth2:
client:
client-id: UserManagement
client-secret: user123
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
jwt:
key-uri: http://localhost:8080/oauth/token_key
这里context-path不要设成/,不然重定向获取code的时候回被拦截
5.3. WebSecurityConfig
package com.cjs.example.config;
import com.cjs.example.util.EnvironmentUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author ChengJianSheng
* @date 2019-03-03
*/
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private EnvironmentUtils environmentUtils;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/bootstrap/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
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
package com.cjs.example.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.Principal;
/**
* @author ChengJianSheng
* @date 2019-03-03
*/
@Controller
@RequestMapping("/member")
public class MemberController {
@GetMapping("/list")
public String list() {
return "member/list";
}
@GetMapping("/info")
@ResponseBody
public Principal info(Principal principal) {
return principal;
}
@GetMapping("/me")
@ResponseBody
public Authentication me(Authentication authentication) {
return authentication;
}
@PreAuthorize("hasAuthority('member:save')")
@ResponseBody
@PostMapping("/add")
public String add() {
return "add";
}
@PreAuthorize("hasAuthority('member:detail')")
@ResponseBody
@GetMapping("/detail")
public String detail() {
return "detail";
}
}
5.5. Order项目跟它是一样的
server:
port: 8083
servlet:
context-path: /orderSystem
security:
oauth2:
client:
client-id: OrderManagement
client-secret: order123
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
jwt:
key-uri: http://localhost:8080/oauth/token_key
5.6. 关于退出
退出就是清空用于与SSO客户端建立的所有的会话,简单的来说就是使所有端点的Session失效,如果想做得更好的话可以令Token失效,但是由于我们用的JWT,故而撤销Token就不是那么容易,关于这一点,在官网上也有提到:
本例中采用的方式是在退出的时候先退出业务服务器,成功以后再回调认证服务器,但是这样有一个问题,就是需要主动依次调用各个业务服务器的logout
6. 工程结构
附上源码: https://github.com/chengjiansheng/cjs-oauth2-sso-demo.git
7. 演示
8. 参考
9. 文档
推荐阅读
(点击标题可跳转阅读)
oAuth2 升级 Spring Cloud Finchley.RELEASE 踩坑分享
单点登录原理与简单实现
jvm 的那些设置参数你都知道吗
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
好文章,我在看❤️
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- beego实现用户未登录跳转到登录页面
- Django实现小程序的登录验证功能,并维护登录态
- 【Python3爬虫】破解时光网登录加密参数并实现模拟登录
- struts实战--登录功能实现
- 单点登录原理与简单实现
- Python实现用户登录问候
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
算法设计、分析与实现
徐子珊 / 2012-10 / 65.00元
《算法设计、分析与实现:c、c++和java》由徐子珊编著,第1章~第6章按算法设计技巧分成渐增型算法、分治算法、动态规划算法、贪婪算法、回溯算法和图的搜索算法。每章针对一些经典问题给出解决问题的算法,并分析算法的时间复杂度。这样对于初学者来说,按照算法的设计方法划分,算法思想的阐述比较集中,有利于快速入门理解算法的精髓所在。一旦具备了算法设计的基本方法,按应用领域划分专题深入学习,读者可以结合已......一起来看看 《算法设计、分析与实现》 这本书的介绍吧!