OAuth2与spring-security-oauth2
- OAuth2
- OAuth2是什么
- OAuth2中4种授权模式作用场景
- spring-security-oauth2
- spring-security-oauth2是什么
- 搭建spring-security-oauth2案例
- 环境版本
- pom.xml
- application.yml
- 目录结构
- 密码模式
- 获取token
- 访问受保护的资源
- 授权码模式
- 获取code
- 获取token
- 访问受保护的资源
- 结语
OAuth2
OAuth2是什么
关于这块概念可以参考: https://www.jianshu.com/p/84a4b4a1e833
OAuth2中4种授权模式作用场景
- 授权码模式
授权码模式一般用于提供给第三方使用 - 简化模式
简化模式一般不会使用 - 密码模式
密码模式一般仅用于系统内部使用 - 客户端凭证模式
客户端凭证模式一般不会使用
spring-security-oauth2
spring-security-oauth2是什么
spring-security-oauth2是基于spring-security框架完整实现oauth2协议的框架,具有oauth2中4种模式访问和第三方登录等功能。
搭建spring-security-oauth2案例
环境版本
<properties>
<java.version>1.8</java.version>
<spring.cloud.version>Hoxton.SR9</spring.cloud.version>
<spring.boot.version>2.3.0.RELEASE</spring.boot.version>
<mysql.connector.version>8.0.15</mysql.connector.version>
</properties>
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://192.168.174.129:3306/oauth?serverTimezone=GMT%2B8&characterEncoding=utf-8&autoReconnect=true&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
目录结构
DemoAuthorizationServerConfiguration:授权服务配置
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class DemoAuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private DataSource dataSource;
private TokenStore tokenStore;
private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private TokenEnhancer tokenEnhancer;
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// @formatter:off
clients.inMemory()
.withClient("test-client").secret("$2a$08$YGw560YLRWHg3Hl29ZlmdOfAeyRQ2u0kDiqUyQ62Y1pkW5n4a.hjO")
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT").redirectUris("http://localhost:8084/oauth/callback")
.scopes("read", "write", "all");// 请求参数scope必须为集合中的某个值
// @formatter:on
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单传参client信息
security.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenEnhancer(tokenEnhancer)
.accessTokenConverter(jwtAccessTokenConverter);
}
}
DemoJwtTokenStoreConfiguration:JwtToken仓库配置
@Configuration
public class DemoJwtTokenStoreConfiguration {
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter tokenConverter) {
return new JwtTokenStore(tokenConverter);
}
@Bean
public JwtAccessTokenConverter tokenConverter() {
JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
tokenConverter.setSigningKey("abc");
return tokenConverter;
}
@Bean
public TokenEnhancer tokenEnhancer(JwtAccessTokenConverter tokenConverter) {
return new DemoJwtTokenEnhancer(tokenConverter);
}
}
DemoResourceServerConfiguration:资源服务配置
@Configuration
@EnableResourceServer
@AllArgsConstructor
public class DemoResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private AccessDeniedHandler accessDeniedHandler;
// private AuthenticationEntryPoint authenticationEntryPoint;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated().and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
// .authenticationEntryPoint(authenticationEntryPoint)
.and().csrf().disable();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
}
DemoWebSecurityConfiguration:WebSecurity配置
@Configuration
public class DemoWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
@Override
@SneakyThrows
public AuthenticationManager authenticationManagerBean() {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A, 8);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().csrf().disable();
}
}
DemoController:受保护的Controller资源
@RestController
@RequestMapping("/api")
public class DemoController {
@RequestMapping("/test")
public Map<String, Object> test() {
Map<String, Object> result = new HashMap<>();
result.put("time", System.currentTimeMillis());
return result;
}
}
DemoClientDetailsService:ClientDetailsService实现类
public class DemoClientDetailsService extends JdbcClientDetailsService {
public DemoClientDetailsService(DataSource dataSource) {
super(dataSource);
}
}
DemoUserDetails:用户信息类
@Getter
public class DemoUserDetails extends User {
/**
* 用户id
*/
private String userId;
/**
* 手机号
*/
private String mobile;
/**
* 是否超级管理员 1是 0否
*/
private Boolean administrator;
/**
* 角色id
*/
private List<Long> roles;
public DemoUserDetails(String userId, String mobile, String username, String password, boolean enabled, List<Long> roles, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities, Boolean administrator) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked,
authorities);
this.userId = userId;
this.roles = roles;
this.administrator = administrator;
this.mobile = mobile;
}
}
DemoUserDetailsServiceImpl:UserDetailsService实现类
@Service
public class DemoUserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return new DemoUserDetails("userId", "mobile", s, "$2a$08$EiGfaup8QkkjT6DhvCSFRuNhdFTEV7Rbu/avUm8lGL2ZUji/lTWji", true, Arrays.asList(1L), true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("1,2"), false);
}
}
DemoAccessDeniedHandler:403处理器
@Component
public class DemoAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString("权限不足,请联系管理员!"));
out.flush();
out.close();
}
}
DemoAuthenticationEntryPoint:401处理器
//@Component
public class DemoAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString("请先登录!"));
out.flush();
out.close();
}
}
DemoJwtTokenEnhancer:TokenEnhancer实现类
@AllArgsConstructor
public class DemoJwtTokenEnhancer implements TokenEnhancer {
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DemoUserDetails principal =
(DemoUserDetails) authentication.getUserAuthentication().getPrincipal();
Map<String, Object> info = new HashMap<>();
info.put("user_id", principal.getUserId() == null ? "" : String.valueOf(principal.getUserId()));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return jwtAccessTokenConverter.enhance(accessToken, authentication);
}
}
DemoPasswordEncoder:PasswordEncoder实现类
public class DemoPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return (String) rawPassword;
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(encode(rawPassword));
}
}
密码模式
获取token
/oauth/token,获取token,access_token就是我们需要在请求头中携带的token。
访问url:
http://localhost:8080/oauth/token?username=admin&password=qwer1234&grant_type=password&scope=all 并在请求头中加入basic auth参数
眼力好的同学已经看出来上图中已经在url中出现了client…
也可以直接访问这个url:
http://localhost:8080/oauth/token?username=admin&password=qwer1234&grant_type=password&scope=all&client_id=test-client&client_secret=abc
这种访问方式又是哪里出来的?第一个java代码块里面就有配置了…
allowFormAuthenticationForClients又是怎么实现的呢?
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单传参client信息
security.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
访问受保护的资源
访问受保护的资源
图片中就是在请求头中将入key为Authorization的值
Authorization Bearer eyJhbGciOiJ…
授权码模式
获取code
浏览器弹出httpBasic验证
输入预设用户名/密码:admin/qwer1234
点击确定,弹出OAuth2用户授权页面,点击授权Authorize
页面重定向到redirect_uri参数指定的url并附带了code
获取token
使用上一步中获取的code替换下面的,并访问url:
http://localhost:8080/oauth/token?grant_type=authorization_code&client_id=test-client&client_secret=abc&redirect_uri=http://localhost:8084/oauth/callback&code=OWwAmR
不会有人直接复制链接不改code参数吧?不会吧,不会吧
访问受保护的资源
该步骤与密码模式中一致
结语
spring-security-oauth2秉承spring一贯风格特色,配置多种多样,如果见到和上面配置不一样很正常,如果想要深入了解,需要仔细阅读源码。上面的案例只是简单的入门描述,由此可以引发很多疑问。
- 使用数据库配置client与user怎么做
- 需要配置手机号验证码登录等其他方式需要怎么做
- 授权码模式需要用户手动授权,如果特殊情况下改为不需要授权怎么做
- 需要在网关中使用,该怎么做
- 需要权限验证,该怎么做
- 基于spring-security-oauth2框架的单点登录怎么做
…