前言
由于公司要求使用Spring-Security-Oauth2快速搭建一套账号中心,此版本为初级版本,仅为作为记录学习下Spring-Security-Oauth2的原理和快速搭建。
开发前准备
在快速搭建之前首先要弄明白Oauth2是什么,大概的运行流程是怎么样子的,而Security是一套Spirng提供的一套安全框架,这个没有接触到的小伙伴可以在这里停住了,由于我已经大概了解是怎么用的,这里就不再过多逼逼。
Oauth简单理解
首先呢Oauth2是一种关于授权的开放网络标准,在讲解运行流程前需要了解几个专用名词
Third-party application:第三方应用程序
HTTP service:HTTP服务提供商
Resource Owner:资源所有者(用户)
User Agent:用户代理(浏览器)
Authorization server:认证服务器
Resource server:资源服务器
来了解上面的专业名词后,来上原理图,好的这就来~
(A) 用户打开client(客户端)以后,客户端要求Resource Owner(用户)给予授权
(B) Resource Owner(用户)统一授权
© client(客户端)使用上一步获得的授权,向认证服务器申请令牌。
(D) Authorization server(认证服务器)对client(客户端)进行认证以后,确认无误,同意发放令牌。
(E) client(客户端)使用令牌,向Resouce Server(资源服务器)申请获取资源。
(F) Resouce Server(资源服务器)确认令牌无误,同意向client(客户端)开放资源。
简单来讲就是,客户端先拿到用户授权,授权后,客户端就可以向认证服务器获取令牌,在通过令牌访问到资源服务器。这里只是简单的描述,有点抽象,需要更详细的了解可以参照理解OAuth 2.0 - 阮一峰.
Oauth2定义了4中授权模式
- 授权码模式
- 简化模式
- 密码模式
- 客户端模式
本文主要讲解的是密码模式哦!
项目搭建
主要依赖如下:
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Security和JWT整合 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${security-oauth2.version}</version>
</dependency>
配置Spring-Security
/**
* 安全认证配置,校验账号密码
*
* @author ikcross
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* 密码加密方式
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/**
* 拦截规则
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 允许所有用户访问"/"和"/index.html"
http.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll();
}
/**
* 验证
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户
// PasswordEncoder 用于密码的加密与比对
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
我们在认证之前,首先要校验用户的账号密码是否正确,在configure(AuthenticationManagerBuilder auth)里面,配置了userDetailsService,这个里面的实现类就是在内存中配置了用户的账号信息,详细可以参考下面的userDetailsService方法,注意这里使用了BCryptPasswordEncoder()进行加密,在Spring2.x中不加密会报错的哦
配置认证服务器
/**
* 认证服务器
*
* @author ikcross
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean(name = "userDetailsServiceImpl")
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
@Bean(name = "clientDetailsServiceImpl")
public ClientDetailsService clientDetailsService() {
return new ClientDetailsServiceImpl();
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
// 开启/oauth/token_key验证端口无权限访问
.tokenKeyAccess("permitAll()")
// 开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("isAuthenticated()");
}
/**
* 用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
/**
* 用来配置令牌端点(Token Endpoint)的安全约束
* <p>
* .authenticationManager:密码模式下配置认证管理器 AuthenticationManager
* .userDetailsService:
* .tokenServices:token保存方式
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService())
.tokenStore(new RedisTokenStore(redisConnectionFactory));
}
}
来到认证服务器,说明用户验证已经完成,这个configure(ClientDetailsServiceConfigurer clients)方法里面配置了clientDetailsService方法,里面配置了需要认证客户端的信息,具体信息在下面配置的clientDetailsService方法在解释吧!
配置资源服务器
/**
* 资源服务器
*
* @author ikcross
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("rId1").stateless(false);
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 所有请求都必须登陆才能访问
// http.authorizeRequests().anyRequest().authenticated();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().anyRequest()
.and()
.anonymous()
.and()
.authorizeRequests()
//配置order访问控制,必须认证过后才可以访问
.antMatchers("/order/**").authenticated();
}
配置userDetailsServiceImpl和clientDetailsServiceImpl
/**
* 客户端详情实现类
*
* @author ikcross
*/
public class ClientDetailsServiceImpl implements ClientDetailsService {
private static final Set<String> RESOURCE_IDS = Sets.newHashSet("rId1");
private static final Set<String> AUTHORIZED_GRANT_TYPES = Sets.newHashSet("password", "refresh_token");
private static final Set<String> SCOPES = Sets.newHashSet("select");
private static final List<GrantedAuthority> AUTHORITIES = AuthorityUtils.createAuthorityList("client");
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
// 写死客户端信息,后续可配置到数据库
BaseClientDetails clientDetails = new BaseClientDetails();
clientDetails.setClientId("client_2");
clientDetails.setResourceIds(RESOURCE_IDS);
clientDetails.setAuthorizedGrantTypes(AUTHORIZED_GRANT_TYPES);
clientDetails.setScope(SCOPES);
clientDetails.setAuthorities(AUTHORITIES);
clientDetails.setClientSecret(passwordEncoder.encode("123456"));
return clientDetails;
}
}
详细的说一下
client_id代表是那个客户端也就是那个App或者Web服务需要认证,
resourceId代表可以访问哪个resourceId
setAuthorizedGrantTypes代表授权类型,这里我用的是密码模式
scopes代表范围
setClientSecret客户端秘钥就是密码
/**
* 用户详情实现类
*
* @author ikcross
*/
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
private static final List<GrantedAuthority> AUTHORITIES = AuthorityUtils.createAuthorityList("USER");
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("UserDetailsServiceImpl.loadUserByUsername");
return User.withUsername("user_2")
.password(passwordEncoder.encode("123456"))
.authorities(AUTHORITIES)
.build();
}
}
loadUserByUsername字面意思,通过username获取用户信息,这里写死了账号密码权限
提供外部接口来测试
/**
* 测试用Controller,提供外部接口测试
*
* @author ikcross
*/
@RestController
public class TestController {
/**
* 商品查询接口:不做安全访问空控制
* @param id
* @return
*/
@RequestMapping("/product/{id}")
public String getProduct(String id) {
//for debug
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "product id : " + id;
}
/**
* 订单查询接口,后续添加访问控制
* @param id
* @return
*/
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
//for debug
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "order id : " + id;
}
}
配完上面的就可以启动项目咯,好耶,终于可以启动了
测试验证
首先是看下获取token:/oauth/token
响应:
再配置中,我们做了/order/**的资源保护,如果直接访问
/product/**没有做任何限制,可以直接访问
带上token就可以访问了,一切完美
以上,一个简单的Spring-Security-Oauth2项目就搭建好了,这只是怎么搭建,还有token的工作原理,怎么创建的,里面有什么信息,内部是如何验证的,等等,如果有时间,我会继续写到文章记录下来。
以上示例的代码可能不完整,完整的初级版本的代码我会放到github上面哦。如果需要可以下载来看下!
启动的话记得启用redis!