通常公司肯定不止一个系统,每个系统都需要进行认证和权限控制,不可能每个每个系统都自己去写,这个时候需要把登录单独提出来
- 登录和授权是统一的
- 业务系统该怎么写还怎么写
最近学习了一下Spring Security,今天用Spring Security OAuth2简单写一个单点登录的示例
在此之前,需要对OAuth2有一点了解
这里有几篇文章可能会对你有帮助
《》
《》
《》
《》
《》
1. 服务器端配置
1.1. Maven依赖
4.0.0 com.cjs.example cjs-oauth2-sso-auth-server 0.0.1-SNAPSHOT jar cjs-oauth2-sso-auth-server org.springframework.boot spring-boot-starter-parent 2.0.3.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-security org.springframework.security.oauth spring-security-oauth2 2.3.3.RELEASE org.springframework.boot spring-boot-starter-thymeleaf org.thymeleaf.extras thymeleaf-extras-springsecurity4 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java 5.1.46 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
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@EnableAuthorizationServerpublic 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@EnableWebSecuritypublic 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;@Servicepublic 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); } Listauthorities = 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;@Controllerpublic class LoginController { @RequestMapping("/login") public String login() { return "login"; } @GetMapping("/index") public String index() { return "index"; }}
1.5. application.yml
server: port: 8080spring: datasource: url: jdbc:mysql://10.123.52.189:3306/oh_coupon username: devdb password: d^V$0Fu!/6-
2. 客户端配置
2.1. Maven依赖
4.0.0 com.example cjs-oauth2-sso-ui 0.0.1-SNAPSHOT jar cjs-oauth2-sso-ui org.springframework.boot spring-boot-starter-parent 2.0.3.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-thymeleaf org.thymeleaf.extras thymeleaf-extras-springsecurity4 org.springframework.security.oauth spring-security-oauth2 2.3.3.RELEASE org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure 2.0.1.RELEASE org.projectlombok lombok true org.springframework.boot spring-boot-maven-plugin
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 Listexport() { 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 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: UISESSIONMEMBERsecurity: 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/melogging: level: root: debugspring: 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. 参考