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

最近學習了一下Spring Security,今天用Spring Security OAuth2簡單寫一個單點登錄的示例
在此之前,需要對OAuth2有一點了解
這里有幾篇文章可能會對你有幫助
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); } }
說明:這里授權服務器我主要是配置了注冊客戶端,客戶端可以從內存中或者數據庫中加載,這里我從數據庫中加載,因為這樣感覺更真實一點兒。
查看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
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. 運行效果
在這個例子中,會員系統(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



