单点登录实现之基于OAuth2.0协议
OAuth2
-
概念
OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息。
OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0 -
应用场景
第三方应用授权登录:在APP或者网页接入一些第三方应用时,时常会需要用户登录另一个合作平台,比如QQ,微博,微信的授权登录,第三方应用通过oauth2方式获取用户信息
开发流程
-
微信开发文档流程说明
-
第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
-
通过code参数加上AppID和AppSecret等,通过API换取access_token;
-
通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
-
-
授权登录流程图
OAuth2实现单点登录
基本介绍
-
实现原理
在OAuth2在有授权服务器、资源服务器、客户端这样几个角色,当我们用它来实现SSO的时候是不需要资源服务器这个角色的,有授权服务器和客户端就够了。
授权服务器当然是用来做认证的,客户端就是各个应用系统,我们只需要登录成功后拿到用户信息以及用户所拥有的权限即可
-
注意事项
之前我一直认为把那些需要权限控制的资源放到资源服务器里保护起来就可以实现权限控制,其实是我想错了,权限控制还得通过Spring Security或者自定义拦截器来做
-
概念介绍
SSO:是一种思想,或者说是一种解决方案,是抽象的,我们要做的就是按照它的这种思想去实现它
OAuth2:是用来允许用户授权第三方应用访问他在另一个服务器上的资源的一种协议,它不是用来做单点登录的,但我们可以利用它来实现单点登录。在本例实现SSO的过程中,受保护的资源就是用户的信息(包括,用户的基本信息,以及用户所具有的权限),而我们想要访问这这一资源就需要用户登录并授权,OAuth2服务端负责令牌的发放等操作
JWT:令牌的生成我们采用JWT,也就是说JWT是用来承载用户的Access_Token的
Spring Security:是用于安全访问的,这里我们我们用来做访问权限控制
相关配置
认证服务器配置
依赖配置
-
pom.xml
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </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> <!-- spring-cloud-starter-oauth2包含了 spring-cloud-starter-security,不用再单独引入 --> <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>org.apache.commons</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>
AuthorizationServerConfig(自定义认证配置类)
介绍
-
参考链接
https://blog.csdn.net/qq_33460562/article/details/79351938
-
AuthorizationServerConfigurerAdapter类
AuthorizationServerConfigurerAdapter类用于配置授权的相关信息,属于OAuth2 配置文件,是配置的核心,主要配置客户端,配置token存储方式等
-
@EnableAuthorizationServer注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class}) public @interface EnableAuthorizationServer { }
-
configure(AuthorizationServerSecurityConfigurer oauthServer)方法
//用来配置令牌端点的安全约束 @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.allowFormAuthenticationForClients();//允许客户端访问 OAuth2 授权接口,否则请求 token 会返回 401。 oauthServer.checkTokenAccess("isAuthenticated()");//允许已授权用户访问 checkToken 接口 oauthServer.tokenKeyAccess("isAuthenticated()");//允许已授权用户访问获取 token 接口 }
-
configure(ClientDetailsServiceConfigurer clients)方法
- 内存中配置客户端详情信息
//用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //使用存在内存中配置两个客户端应用的通行证,相当于硬编码了,正式环境下的做法是持久化到数据库中,比如 mysql 中 clients.inMemory() .withClient("sheep1") .secret(new BCryptPasswordEncoder().encode("123456")) .authorizedGrantTypes("authorization_code", "refresh_token") .redirectUris("http://localhost:8086/login") .scopes("all") .autoApprove(false) .and() .withClient("sheep2") .secret(new BCryptPasswordEncoder().encode("123456")) .authorizedGrantTypes("authorization_code", "refresh_token") .redirectUris("http://localhost:8087/login") .scopes("all") .autoApprove(false); }
- 官方数据库结构中配置客户端详情信息
@Autowired private DataSource dataSource; //用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } //数据库表结构见下面
- 自定义数据库结构中配置客户端详情信息
//用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,可以通过自定义数据库来存储调取详情信息 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 配置客户端应用通信证, 用于client认证 clients.withClientDetails(getClientDetails()); } @Bean // 声明ApplyClientDetailService public ApplyClientDetailService getClientDetails() { return new ApplyClientDetailService(); }
ApplyClientDetailService自定义获取客户端详情类
import org.apache.tomcat.jdbc.pool.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.ClientRegistrationException; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import oauth.security.client.service.ApplyService; /** * * 1. 在配置客户端中,使用了ApplyClientDetailService类,是自定义的获取Client的一个类,实现ClientDetailsService接口, * 重写loadClientByClientId方法 * * 2. 对Client的访问主要依靠JdbcClientDetailsService类的实现,必须使用官方给出的数据库结构, * 如果想自定义数据库结构,可以根据需求重写ClientDetailsService类的实现 **/ public class ApplyClientDetailService implements ClientDetailsService { @Autowired private ApplyService applyService; @Autowired private DataSource dataSource; @Override public ClientDetails loadClientByClientId(String applyName) throws ClientRegistrationException { /* // 使用mybatic验证client是否存在 ,根据需求写sql Map clientMap = applyService.findApplyById(applyName); if(clientMap == null) { throw new ClientRegistrationException("应用" + applyName + "不存在!"); }*/ // MyJdbcClientDetailsService jdbcClientDetailsService= new MyJdbcClientDetailsService(dataSource, "authentication"); JdbcClientDetailsService jdbcClientDetailsService= new JdbcClientDetailsService(dataSource); ClientDetails clientDetails = jdbcClientDetailsService.loadClientByClientId(applyName); return clientDetails; } }
ClientDetailsServiceConfigurer参数的重写,在这里定义各个端的约束条件:
-
ClientId、Client-Secret:这两个参数对应请求端定义的 cleint-id 和 client-secret
-
authorizedGrantTypes 可以包括如下几种设置中的一种或多种:
authorization_code:授权码类型。
implicit:隐式授权类型。
password:资源所有者(即用户)密码类型。
client_credentials:客户端凭据(客户端ID以及Key)类型。
refresh_token:通过以上授权获得的刷新令牌来获取新的令牌。
accessTokenValiditySeconds:token 的有效期 -
scopes:用来限制客户端访问的权限,在换取的 token 的时候会带上 scope 参数,只有在 scopes 定义内的,才可以正常换取 token。
-
configure(AuthorizationServerEndpointsConfigurer endpoints)方法
- 数据库保存token
@Autowired AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; // 初始化JdbcTokenStore @Autowired public TokenStore getTokenStore() { return new JdbcTokenStore(dataSource); } //用来配置令牌(token)的访问端点和令牌服务(token services) @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(getTokenStore()) // 数据库保存token .authenticationManager(authenticationManager); //调用此方法才能支持 password 模式。 }
- Redis保存token
@Autowired AuthenticationManager authenticationManager; @Autowired RedisConnectionFactory redisConnectionFactory; //用来配置令牌(token)的访问端点和令牌服务(token services) @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(new RedisTokenStore(redisConnectionFactory))// 指定 token 的存储方式,redis保存token .authenticationManager(authenticationManager); //调用此方法才能支持 password 模式。 }
- 普通JWT保存token
@Autowired AuthenticationManager authenticationManager; @Autowired public UserDetailsService nbspUserDetailsService; //用来配置令牌(token)的访问端点和令牌服务(token services) @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(jwtTokenStore()) // 指定 token 的存储方式,普通jwt方式存储 .accessTokenConverter(jwtAccessTokenConverter()) .userDetailsService(nbspUserDetailsService)//设置用户验证服务,自定义用户实现类实现 UserDetailsService接口,重写loadUserByUsername方法,返回用户信息对象 .authenticationManager(authenticationManager);//调用此方法才能支持 password 模式 } @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev"); return accessTokenConverter; }
- 增强JWT保存token
// 由于JWT配置已经抽取至JwtTokenConfig配置类,项目启动后预先加载,这里可以直接注入 @Autowired private TokenStore jwtTokenStore; @Autowired//默认按类型注入,若存在多个实例,需要结合@Qualifier注解指定注入Bean的名称 private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private TokenEnhancer jwtTokenEnhancer; @Autowired AuthenticationManager authenticationManager; @Autowired public UserDetailsService nbspUserDetailsService; //用来配置令牌(token)的访问端点和令牌服务(token services) @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancerList = new ArrayList<>(); enhancerList.add(jwtTokenEnhancer); enhancerList.add(jwtAccessTokenConverter); enhancerChain.setTokenEnhancers(enhancerList); endpoints.tokenStore(jwtTokenStore)// 指定 token 的存储方式,增强jwt方式存储 .accessTokenConverter(jwtAccessTokenConverter) .tokenEnhancer(enhancerChain) .userDetailsService(nbspUserDetailsService)//设置用户验证服务,自定义用户实现类实现 UserDetailsService接口,重写loadUserByUsername方法,返回用户信息对象 .authenticationManager(authenticationManager);//调用此方法才能支持 password 模式 }
JwtTokenConfig配置类
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.provider.token.TokenEnhancer; 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; //自动注入JWT配置 @Configuration public class JwtTokenConfig { @Bean//@Bean就放在方法上,就是让方法去产生一个Bean,然后交给Spring容器 public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev");// Sets the JWT signing key return accessTokenConverter; } @Bean public TokenEnhancer jwtTokenEnhancer(){ return new JWTokenEnhancer(); } }
JWTokenEnhancer增强配置类
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import java.util.HashMap; import java.util.Map; /** * 如果想在 JWT 中加入额外的字段(比方说用户的其他信息), * 需要自定义增强实现类,实现 spring security oauth2提供的 TokenEnhancer 增强器接口 * 重写enhance方法(RedisToken 的方式同样可以) * */ public class JWTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { Map<String, Object> info = new HashMap<>(); info.put("jwt-ext", "JWT 扩展信息"); ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info); return oAuth2AccessToken; } }
-
常见自定义重写实现类
- 重写UserDetailsService接口实现类
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; /** * * 一般需要重写,自定义用户详情实现类,需要实现UserDetailsService接口,重写loadUserByUsername方法, * 通过用户名查找用户信息,并返回用户详情信息,包括用户名,加密后的密码,和权限集合 * */ @Slf4j @Service public class NbspUserDetailsService 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<SysPermission> permissionList = permissionService.findByUserId(sysUser.getId()); List<SimpleGrantedAuthority> 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; //返回默认用户详情信息 //return new org.springframework.security.core.userdetails.User(username,password, authorities); } }
- 重写User类
import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import java.util.Collection; /** * 大部分时候直接用User即可不必扩展 * */ @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); } }
案例代码
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 CharlesYan
* @date 2020-04-17
*/
@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 {
//普通jwt方式保存token
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;
}
}
WebSecurityConfig(自定义安全配置类)
介绍
-
参考链接
https://blog.csdn.net/sinat_29899265/article/details/80736498
-
WebSecurityConfigurerAdapter类
WebSecurityConfigurerAdapter类是个适配器,属于spring security 基础配置,在配置的时候,需要我们自己写个配置类去继承它,然后编写自己所特殊需要的配置
-
@EnableWebSecurity注解
//@EnableWebSecurity完成的工作便是加载了WebSecurityConfiguration,AuthenticationConfiguration这两个核心配置类,也就此将spring security的职责划分为了配置安全信息,配置认证信息两部分 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class}) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { boolean debug() default false; }
-
configure(HttpSecurity http)方法
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //1. 配置Security的认证策略(定义授权规则), 每个模块配置使用and结尾 @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests()//2. authorizeRequests()配置路径拦截,表明路径访问所对应的权限,角色,认证信息。 .antMatchers("/resources/**", "/signup", "/about").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") .anyRequest().authenticated() .and() .formLogin()//3. formLogin()对应表单认证相关的配置 .usernameParameter("username") .passwordParameter("password") .failureForwardUrl("/login?error") .loginPage("/login") .permitAll() .and() .logout()//4. logout()对应了注销相关的配置 .logoutUrl("/logout") .logoutSuccessUrl("/index") .permitAll() .and() .httpBasic()//5. httpBasic()可以配置basic登录 .disable(); } }
-
configure(AuthenticationManagerBuilder auth)方法
- 在内存中配置用户认证信息
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //1. 配置的是认证信息(定义认证规则) //作用:AuthenticationManagerBuilder 这个类就是AuthenticationManager的建造者, 我们只需要向这个类中, 配置用户信息, 就能生成对应的AuthenticationManager, 这个类是用户身份的管理者, 是认证的入口, 因此,我们需要通过这个配置,向security提供真实的用户身份。 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication()//2. 设置从缓存中获取已事先定义好的认证信息(配置一个内存中的用户认证器) .withUser("admin").password("admin").roles("USER"); } }
- 数据库中获取认证信息
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private NbspUserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } //1. 配置的是认证信息(定义认证规则) //作用:AuthenticationManagerBuilder 这个类就是AuthenticationManager的建造者, 我们只需要向这个类中, 配置用户信息, 就能生成对应的AuthenticationManager, 这个类是用户身份的管理者, 是认证的入口, 因此,我们需要通过这个配置,向security提供真实的用户身份。 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService)//3. userDetailsService是用户自定义用户详情服务类, 这个类的作用就是去获取用户信息,比如从数据库中获取。 这样的话,AuthenticationManager在认证用户身份信息的时候,就会从中获取用户身份,和从http中拿的用户身份做对比 .passwordEncoder(passwordEncoder()); } //注意事项:UserDetailService会默认加载DaoAuthenticationProvider }
-
configure(WebSecurity web)方法
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //全局请求忽略规则配置(比如说静态文件,比如说注册页面) @Override public void configure(WebSecurity web) throws Exception { web .ignoring()//一般只需要重写ignoring()配置 .antMatchers("/static/**"); } }
案例代码
import com.cjs.sso.service.NbspUserDetailsService;
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 CharlesYan
* @date 2019-02-11
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private NbspUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@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();
}
}
客户端详细信息表(官方)
-
建表语句
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) )ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true); INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
-
注意事项
client_secret 字段不能直接是 secret 的原始值,需要经过加密。因为是用的 BCryptPasswordEncoder,所以最终插入的值应该是经过 BCryptPasswordEncoder.encode()之后的值。
核心配置文件
-
application.yml
spring: datasource: url: jdbc:mysql://localhost:3306/nbsp?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 1q2w3e4r # driver-class-name: com.mysql.jdbc.Driver jpa: show-sql: true session: store-type: redis redis: host: 192.168.159.133 password: redispassword port: 6379 server: port: 8080
-
注意事项
- Spring Boot 2.0 之后默认使用 hikari 作为数据库连接池。如果使用其他连接池需要引入相关包,然后对应的增加配置。
客户端配置
依赖配置
-
pom.xml
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </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>Demo project for Spring Boot</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>
WebSecurityConfig(自定义安全配置类)
ResourceServerConfig(资源配置类)
-
Redis保存Token
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Value("${security.oauth2.client.client-id}") private String clientId; @Value("${security.oauth2.client.client-secret}") private String secret; @Value("${security.oauth2.authorization.check-token-access}") private String checkTokenEndpointUrl; @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore (){ return new RedisTokenStore(redisConnectionFactory); } @Bean public RemoteTokenServices tokenService() { RemoteTokenServices tokenService = new RemoteTokenServices(); tokenService.setClientId(clientId); tokenService.setClientSecret(secret); tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl); return tokenService; } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenServices(tokenService()); } }
-
JWT保存Token
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev"); accessTokenConverter.setVerifierKey("dev"); return accessTokenConverter; } @Autowired private TokenStore jwtTokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(jwtTokenStore); } }
介绍
-
相关注解
在 OAuth2 的概念里,所有的接口都被称为资源,接口的权限也就是资源的权限,所以 Spring Security OAuth2 中提供了关于资源的注解 @EnableResourceServer 和 @EnableWebSecurity的作用类似
注意事项
- @EnableOAuth2Sso注解
- 引入依赖
<!-- Spring Boot 2.x 中没有@EnableOAuth2Sso这个注解 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
-
源码
-
简介
对于OAuth 2.0客户端配置,简化的配置用@EnableOAuth2Client。这个注解做两件事情:
(1) 创建一个过滤器(ID是oauth2ClientContextFilter)来存储当前的请求和上下文。在请求期间需要进行身份认证时,它管理重定向URI。
(2) 在请求范围内创建一个AccessTokenRequest类型的bean。对于授权代码(或隐式)授予客户端是很有用的,可以避免与单个用户相关的状态发生冲突。
-
参考链接
https://projects.spring.io/spring-security-oauth/docs/oauth2.html
https://www.cnblogs.com/trust-freedom/p/12002089.html
https://www.jianshu.com/p/fe1194ca8ecd
-
其他
-
antMatcher()
是HttpSecurity的一个方法,他只告诉了Spring我只配置了一个我这个Adapter能处理哪个的url,它与authorizeRequests()没有任何关系。
-
authorizeRequests().antMatchers()
是在antMatchers()中指定的一个或多个路径,比如执行permitAll()或hasRole()。他们在第一个http.antMatcher()匹配时就会生效。
所以,WebSecurityConfigurerAdapter与ResourceServerConfigurerAdapter同时使用,其实和spring security的多个HttpSecurity配置是一样的,原理也差不多是一样的。
-
案例代码
-
ClientWebsecurityConfigurer类
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.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableOAuth2Sso public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.antMatcher("/**").authorizeRequests() .anyRequest().authenticated(); } }
参考链接
-
OAuth2实现单点登录SSO
https://www.cnblogs.com/cjsblog/p/10548022.html
-
Spring Cloud OAuth2 实现用户认证及单点登录
https://www.cnblogs.com/fengzheng/p/11724625.html
-
OAuth2介绍与使用
https://www.jianshu.com/p/4f5fcddb4106