一、概念部分
1.为什么需要做一个单独的认证授权服务?
为了保证服务对外的安全性,往往都会在服务接口采用权限校验机制,为了防止客户端在发起请求中途被篡改数据等安全方面的考虑,还会有一些签名校验的机制。
在分布式微服务架构的系统中,我们把原本复杂的系统业务拆分成了若干个独立的微服务应用,我们不得不在每个微服务中都实现这样一套校验逻辑,这样就会有很多的代码和功能冗余,随着服务的扩大和业务需求的复杂度不断变化,修改校验逻辑变得相当麻烦,一处改,处处改。所以我们需要把认证授权服务单独出来,做成一个服务进行调用。
2.授权服务的使用场景有哪些?
授权服务并不是每个应用的接口直接去调用,判断哪些用户有权限访问接口。 而是通过API网关进行统一调用。用户所有的请求都必须先通过API网关,API网关在进行路由转发之前对该请求进行前置校验,我们可以方便的使用OAuth2认证授权服务来做单点登录等操作。
可以使用OAuth2来实现对多个服务的统一认证授权。
3.本项目的主要操作流程
关于OAuth2的协议此处不再介绍,本项目演示主要的认证和授权步骤,简单来说就是客户端根据约定的ClientID、ClientSecret、Scope来从Access Token URL地址获取AccessToken,并经过AuthURL认证,用得到的AccessToken来访问其他资源接口。
项目结构如下,其中核心部分为core包下的config包中的三个配置类
从上往下分别是是授权服务配置,资源服务配置和安全配置
二、代码示例
1.新建一个SpringBoot项目 auth-server,在项目中添加依赖
build.gradle
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-oauth2')
compile('org.springframework.cloud:spring-cloud-starter-security')
compile('org.springframework.boot:spring-boot-starter-data-mongodb')
compile('org.springframework.boot:spring-boot-starter-data-redis')
compile('org.springframework.cloud:spring-cloud-starter-eureka')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
2.新建配置文件 application.yml
#服务器配置
server:
#端口
port: 8020
#服务器发现注册配置
eureka:
client:
serviceUrl:
#配置服务中心(可配置多个,用逗号隔开)
defaultZone: https://www.apiboot.cn/eureka
#spring配置
spring:
#应用配置
application:
#名称: OAuth2认证授权服务
name: auth-server
#数据库配置
data:
mongodb:
port: 27017
database: auth_server
#安全配置
security:
#oauth2配置
oauth2:
resource:
filter-order: 3
3.应用启动类添加注解
/**
* OAuth2认证授权服务
* @ EnableDiscoveryClient 启用服务注册发现
*/
@SpringBootApplication
@EnableDiscoveryClient
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
4.新建账户实体类 Account.java
/**
* 账户实体类
*/
public class Account {
@Id
private String id; // 主键
private String userName; // 用户名
private String passWord; // 密码
private String[] roles; // 角色
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public String[] getRoles() {
return roles;
}
public void setRoles(String[] roles) {
this.roles = roles;
}
}
5.新建账户表数据操作类,MongoDB操作接口 AccountRepository.java
/**
* 账户数据库操作类
* MongoDB操作接口
*/
@Component
public interface AccountRepository extends MongoRepository<Account, String> {
/**
* 根据用户名查找账户信息
* @param username 用户名
* @return 账户信息
*/
Account findByUserName(String username);
}
6.新建用户信息控制器 UserController.java
/**
* 用户信息控制器
*/
@RestController
public class UserController {
@Autowired
private AccountRepository accountRepository; // 账户数据操作
/**
* 初始化用户数据
*/
@Autowired
public void init(){
// 为了方便测试,这里添加了两个不同角色的账户
accountRepository.deleteAll();
Account accountA = new Account();
accountA.setUserName("admin");
accountA.setPassWord("admin");
accountA.setRoles(new String[]{"ROLE_ADMIN","ROLE_USER"});
accountRepository.save(accountA);
Account accountB = new Account();
accountB.setUserName("guest");
accountB.setPassWord("pass123");
accountB.setRoles(new String[]{"ROLE_GUEST"});
accountRepository.save(accountB);
}
/**
* 获取授权用户的信息
* @param user 当前用户
* @return 授权信息
*/
@GetMapping("/user")
public Principal user(Principal user){
return user;
}
}
7.新建用户信息服务类,实现 Spring Security的UserDetailsService接口方法,用于身份认证 DomainUserDetailsService.java
/**
* 用户信息服务
* 实现 Spring Security的UserDetailsService接口方法,用于身份认证
*/
@Service
public class DomainUserDetailsService implements UserDetailsService {
@Autowired
private AccountRepository accountRepository; // 账户数据操作接口
/**
* 根据用户名查找账户信息并返回用户信息实体
* @param username 用户名
* @return 用于身份认证的 UserDetails 用户信息实体
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountRepository.findByUserName(username);
if (account!=null){
return new User(account.getUserName(),account.getPassWord(), AuthorityUtils.createAuthorityList(account.getRoles()));
}else {
throw new UsernameNotFoundException("用户["+username+"]不存在");
}
}
}
8.新建授权服务配置类,继承AuthorizationServerConfigurerAdapter
AuthorizationServerConfig.java
/**
* 授权服务器配置
* @ EnableAuthorizationServer 启用授权服务
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager; // 认证管理器
@Autowired
private RedisConnectionFactory redisConnectionFactory; // redis连接工厂
/**
* 令牌存储
* @return redis令牌存储对象
*/
@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(this.authenticationManager);
endpoints.tokenStore(tokenStore());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("android")
.scopes("xx")
.secret("android")
.authorizedGrantTypes("password", "authorization_code", "refresh_token")
.and()
.withClient("webapp")
.scopes("xx")
.authorizedGrantTypes("implicit");
}
}
9.新建资源服务配置类,继承ResourceServerConfigurerAdapter
ResourceServerConfig.java
/**
* 资源服务配置
* @ EnableResourceServer 启用资源服务
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new OAuth2RequestedMatcher())
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
}
/**
* 定义OAuth2请求匹配器
*/
private static class OAuth2RequestedMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
String auth = request.getHeader("Authorization");
//判断来源请求是否包含oauth2授权信息,这里授权信息来源可能是头部的Authorization值以Bearer开头,或者是请求参数中包含access_token参数,满足其中一个则匹配成功
boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
boolean haveAccessToken = request.getParameter("access_token")!=null;
return haveOauth2Token || haveAccessToken;
}
}
}
10.新建安全配置类,继承WebSecurityConfigurerAdapter
SecurityConfig.java
/**
* 安全配置
* @ EnableWebSecurity 启用web安全配置
* @ EnableGlobalMethodSecurity 启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 注入用户信息服务
* @return 用户信息服务对象
*/
@Bean
public UserDetailsService userDetailsService() {
return new DomainUserDetailsService();
}
/**
* 全局用户信息
* @param auth 认证管理
* @throws Exception 用户认证异常信息
*/
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
/**
* 认证管理
* @return 认证管理对象
* @throws Exception 认证异常信息
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* http安全配置
* @param http http安全对象
* @throws Exception http安全异常信息
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated().and()
.httpBasic().and().csrf().disable();
}
}
三、演示
假设我们现在要直接访问 /user接口,我们打开PoatMan直接请求该接口
发现返回了401状态码,并且报了Unauthorized 未经授权的错误,提示信息显示 访问此资源需要完全身份验证。 添加了Spring Security+OAuth2后,所有的资源访问都需要通过token
1.获取access_token
启动服务,打开PostMan切换到Authorization页卡,Type类型选择Basic Auth,Username和Password填写授权服务配置中对应的withClient和secret的值,这里都写android
点击Update Request,切换到Headers页卡,发现请求头里多了个Authorization参数,参数值就是根据Authorization页卡填写的授权信息生成的,要获取token必须有该参数值
使用post方法访问授权服务的 /oauth/token地址,post参数需要填写grant_type、username、password。点击send请求,将会返回如下access_token信息。
拿到access_token后,就可以在请求其他资源接口的时候携带上该token参数值获取该角色可获取的资源,打开浏览器访问 /user接口并携带上 access_token参数值
2.使用正确的姿势获取access_token,并根据access_token获取资源
切换到Authorization页卡,选择OAuth2.0,点击Get New Access Token
TokenName可以随意填写,其他信息根据实际情况填写。点击Request Token后,将会跳出输入用户名和密码的页面(这个操作其实就是根据用户名和密码登录并获取AccessToken)
登录成功后,我们看到左侧有个我们刚刚新建的auth,点击auth,右侧会显示该请求获取到的AccessToken信息。点击UseToken
点击Use Token后,发现请求头Headers页卡里添加了Authorization的参数值
点击Send,请求/user接口,正常返回用户授权信息
到此 OAuth2认证授权服务已经搭建完毕了,关于api-gateway和OAuth2认证授权服务的整合调用,会在下一篇文章中写到,敬请期待。
项目地址:https://github.com/lanshiqin/cloud-project
欢迎点赞
网友评论