通常公司肯定不止一個系統,每個系統都需要進行認證和權限控制,不可能每個每個系統都自己去寫,這個時候需要把登入單獨提出來
- 登入和授權是統一的
- 業務系統該怎麼寫還怎麼寫

最近學習了一下Spring Security,今天用Spring Security OAuth2簡單寫一個單點登入的示例
在此之前,需要對OAuth2有一點了解
這裡有幾篇文章可能會對你有幫助
《Spring Boot Security》
《OAuth 2.0》
《OAuth 2.0 授權碼請求》
《Spring Security OAuth 2.0》
《Spring Boot OAuth 2.0 用戶端》
1. 伺服器端配置
1.1. Maven依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cjs.example</groupId>
<artifactId>cjs-oauth2-sso-auth-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cjs-oauth2-sso-auth-server</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.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-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</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>
1.2. 配置授權伺服器
package com.cjs.example.config;
import org.springframework.context.annotation.Configuration;
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 javax.annotation.Resource;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private DataSource dataSource;
/**
* 配置授權伺服器的安全,意味着實際上是/oauth/token端點。
* /oauth/authorize端點也應該是安全的
* 預設的設定覆寫到了絕大多數需求,是以一般情況下你不需要做任何事情。
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
}
/**
* 配置ClientDetailsService
* 注意,除非你在下面的configure(AuthorizationServerEndpointsConfigurer)中指定了一個AuthenticationManager,否則密碼授權方式不可用。
* 至少配置一個client,否則伺服器将不會啟動。
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
/**
* 該方法是用來配置Authorization Server endpoints的一些非安全特性的,比如token存儲、token自定義、授權類型等等的
* 預設情況下,你不需要做任何事情,除非你需要密碼授權,那麼在這種情況下你需要提供一個AuthenticationManager
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
}
}
說明:這裡授權伺服器我主要是配置了注冊用戶端,用戶端可以從記憶體中或者資料庫中加載,這裡我從資料庫中加載,因為這樣感覺更真實一點兒。
![]()
Spring Security OAuth2 SSO
檢視JdbcClientDetailsService源碼我們不難看出其表結構。(PS:也可以自定義,就像UserDetailsService那樣)
這裡,我準備的SQL腳本如下:
CREATE TABLE oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, autoapprove)
VALUES ('MemberSystem', '$2a$10$dYRcFip80f0jIKGzRGulFelK12036xWQKgajanfxT65QB4htsEXNK', 'user_info', 'authorization_code', 'http://localhost:8081/login', 'user_info');
INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, autoapprove)
VALUES ('CouponSystem', '$2a$10$dYRcFip80f0jIKGzRGulFelK12036xWQKgajanfxT65QB4htsEXNK', 'user_info', 'authorization_code', 'http://localhost:8082/login', 'user_info');
這裡注冊了兩個用戶端,分别是MemberSystem和CouponSystem。
1.3. 配置WebSecurity
package com.cjs.example.config;
import com.cjs.example.support.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**","/login/**", "/logout").permitAll()
.anyRequest().authenticated() // 其他位址的通路均需驗證權限
.and()
.formLogin()
.loginPage("/login")
.and()
.logout().logoutSuccessUrl("/");
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
說明:
- 這裡,主要配置了UserDetailsService
package com.cjs.example.support;
import com.cjs.example.domain.SysPermission;
import com.cjs.example.domain.SysRole;
import com.cjs.example.domain.SysUser;
import com.cjs.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 授權的時候是對角色授權,而認證的時候應該基于資源,而不是角色,因為資源是不變的,而使用者的角色是會變的
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userService.getUserByName(username);
if (null == sysUser) {
throw new UsernameNotFoundException(username);
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (SysRole role : sysUser.getRoleList()) {
for (SysPermission permission : role.getPermissionList()) {
authorities.add(new SimpleGrantedAuthority(permission.getCode()));
}
}
return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
}
}
1.4. 建立登入頁面
package com.cjs.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
@RequestMapping("/login")
public String login() {
return "login";
}
@GetMapping("/index")
public String index() {
return "index";
}
}
1.5. application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://10.123.52.189:3306/oh_coupon
username: devdb
password: d^V$0Fu!/6-<s
driver-class-name: com.mysql.jdbc.Driver
logging:
level:
root: debug
2. 用戶端配置
2.1. Maven依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>cjs-oauth2-sso-ui</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cjs-oauth2-sso-ui</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2. WebSecurity配置
package com.cjs.example.config;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableOAuth2Sso
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UiSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**").permitAll()
.anyRequest()
.authenticated();
}
}
這裡最重要的是應用了@EnableOAuth2Sso注解
Spring Boot 1.x 版本和 2.x 版本在OAuth2這一塊的差異還是比較大的,在Spring Boot 2.x 中沒有@EnableOAuth2Sso這個注解,是以我引用了spring-security-oauth2-autoconfigure
![]()
Spring Security OAuth2 SSO
2.3. 定義一個簡單的控制器
package com.cjs.example.controller;
import com.cjs.example.domain.Member;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import java.util.ArrayList;
import java.util.List;
@Controller
@RequestMapping("/member")
public class MemberController {
/**
* 會員清單頁面
*/
@RequestMapping("/list")
public ModelAndView list() {
ModelAndView modelAndView = new ModelAndView("member/list");
return modelAndView;
}
/**
* 導出
*/
@PreAuthorize("hasAuthority('memberExport')")
@ResponseBody
@RequestMapping("/export")
public List<Member> export() {
Member member = new Member();
member.setName("蘇九兒");
member.setCode("1000");
member.setMobile("13112345678");
member.setGender(1);
Member member1 = new Member();
member1.setName("郭雙");
member1.setCode("1001");
member1.setMobile("15812346723");
member1.setGender(1);
List<Member> list = new ArrayList<>();
list.add(member);
list.add(member1);
return list;
}
/**
* 詳情
*/
@PreAuthorize("hasAuthority('memberDetail')")
@RequestMapping("/detail")
public ModelAndView detail() {
return new ModelAndView(" member/detail");
}
}
2.4. application.yml
server:
port: 8081
servlet:
session:
cookie:
name: UISESSIONMEMBER
security:
oauth2:
client:
client-id: MemberSystem
client-secret: 12345
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
user-info-uri: http://localhost:8080/user/me
logging:
level:
root: debug
spring:
thymeleaf:
cache: false
- 這裡需要注意的是不要忘記設定cookie-name,不然會有一些莫名其妙的問題,比如“User must be authenticated with Spring Security before authorization can be completed”
3. 運作效果
![]()
Spring Security OAuth2 SSO
在這個例子中,會員系統(localhost:8081)和營銷系統(localhost:8082)是兩個系統
可以看到,當我們登入會員系統以後,再進營銷系統就不需要登入了。
3.1. 遺留問題
- 退出
- 記住我
3.2. 工程結構
https://github.com/chengjiansheng/cjs-oauth2-example.git
3.3. 參考
https://github.com/eugenp/tutorials/tree/master/spring-security-sso
https://blog.csdn.net/sinat_24798023/article/details/80536881
https://segmentfault.com/a/1190000012384850
http://www.baeldung.com/spring-security-oauth-revoke-tokens
https://segmentfault.com/a/1190000013531863
https://spring.io/guides/tutorials/spring-boot-oauth2/