基于Spring Security的Oauth2授权实现

前言

经过一段时间的学习Oauth2,在网上也借鉴学习了一些大牛的经验,推荐在学习的过程中多看几遍阮一峰的《理解OAuth 2.0》,经过对Oauth2的多种方式的实现,个人推荐Spring Security和Oauth2的实现是相对优雅的,理由如下:

1、相对于直接实现Oauth2,减少了很多代码量,也就减少的查找问题的成本。

2、通过调整配置文件,灵活配置Oauth相关配置。

3、通过结合路由组件(如zuul),更好的实现微服务权限控制扩展。

 

Oauth2概述

oauth2根据使用场景不同,分成了4种模式

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

在项目中我们通常使用授权码模式,也是四种模式中最复杂的,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。

Oauth2授权主要由两部分组成:

  • Authorization server:认证服务
  • Resource server:资源服务

在实际项目中以上两个服务可以在一个服务器上,也可以分开部署。

准备阶段

核心maven依赖如下

        
			org.springframework.boot
			spring-boot-starter-web
		

		
			com.fasterxml.jackson.datatype
			jackson-datatype-joda
		
		
			org.thymeleaf.extras
			thymeleaf-extras-springsecurity4
		
		
			org.springframework.boot
			spring-boot-starter-thymeleaf
		
		
			org.springframework.boot
			spring-boot-starter-security
		
		
			org.springframework.security.oauth
			spring-security-oauth2
		

		
			org.springframework.boot
			spring-boot-starter-jdbc
		
		
			mysql
			mysql-connector-java
		
		
			org.springframework.boot
			spring-boot-starter-data-jpa
		

token的存储主流有三种方式,分别为内存、redis和数据库,在实际项目中通常使用redis和数据库存储。个人推荐使用mysql数据库存储。

初始化数据结构、索引和数据SQL语句如下:

--
--  Oauth sql  -- MYSQL
--

Drop table  if exists oauth_client_details;
create table oauth_client_details (
  client_id VARCHAR(255) PRIMARY KEY,
  resource_ids VARCHAR(255),
  client_secret VARCHAR(255),
  scope VARCHAR(255),
  authorized_grant_types VARCHAR(255),
  web_server_redirect_uri VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information TEXT,
  autoapprove VARCHAR (255) default 'false'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


Drop table  if exists oauth_access_token;
create table oauth_access_token (
  token_id VARCHAR(255),
  token BLOB,
  authentication_id VARCHAR(255),
  user_name VARCHAR(255),
  client_id VARCHAR(255),
  authentication BLOB,
  refresh_token VARCHAR(255)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


Drop table  if exists oauth_refresh_token;
create table oauth_refresh_token (
  token_id VARCHAR(255),
  token BLOB,
  authentication BLOB
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


Drop table  if exists oauth_code;
create table oauth_code (
  code VARCHAR(255),
  authentication BLOB
) ENGINE=InnoDB DEFAULT CHARSET=utf8;



-- Add indexes
create index token_id_index on oauth_access_token (token_id);
create index authentication_id_index on oauth_access_token (authentication_id);
create index user_name_index on oauth_access_token (user_name);
create index client_id_index on oauth_access_token (client_id);
create index refresh_token_index on oauth_access_token (refresh_token);
create index token_id_index on oauth_refresh_token (token_id);
create index code_index on oauth_code (code);

-- INSERT DEFAULT DATA
INSERT INTO `oauth_client_details` VALUES ('dev', '', 'dev', 'app', 'authorization_code', 'http://localhost:7777/', '', '3600', '3600', '{"country":"CN","country_code":"086"}', 'TAIJI');

核心配置

核心配置主要分为授权应用和客户端应用两部分,如下:

  • 授权应用:即Oauth2授权服务,主要包括Spring Security、认证服务和资源服务两部分配置
  • 客户端应用:即通过授权应用进行认证的应用,多个客户端应用间支持单点登录

授权应用主要配置如下:

application.properties链接已初始化Oauth2的数据库即可

Application启动类,授权服务开启配置和Spring Security配置,如下:

@SpringBootApplication
@AutoConfigureAfter(JacksonAutoConfiguration.class)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
@EnableAuthorizationServer
public class Application extends WebSecurityConfigurerAdapter {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    // 启动的时候要注意,由于我们在controller中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例
    @Autowired
    private RestTemplateBuilder builder;

    // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
    @Bean
    public RestTemplate restTemplate() {
        return builder.build();
    }

    @Configuration
    public class WebMvcConfig extends WebMvcConfigurerAdapter {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/login").setViewName("login");
        }
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.headers().frameOptions().disable();
        http.authorizeRequests()
                .antMatchers("/403").permitAll() // for test
                .antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access", "/appManager").permitAll() // for login
                .antMatchers("/image", "/js/**", "/fonts/**").permitAll() // for login
                .antMatchers("/j_spring_security_check").permitAll()
                .antMatchers("/oauth/authorize").authenticated();
        /*.anyRequest().fullyAuthenticated();*/
        http.formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
                .and()
                .authorizeRequests().anyRequest().authenticated()
                .and().logout().invalidateHttpSession(true)
                .and().sessionManagement().maximumSessions(1).expiredUrl("/login?expired").sessionRegistry(sessionRegistry());
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        http.rememberMe().disable();
        http.httpBasic();

    }

}

资源服务开启,如下:

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();
        }
    }

OAuth2认证授权服务配置,如下:

@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
	public static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);

	@Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }


    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.authenticationManager(authenticationManager);
        endpoints.tokenStore(tokenStore());
        // 配置TokenServices参数
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(false);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        tokenServices.setAccessTokenValiditySeconds( (int) TimeUnit.MINUTES.toSeconds(10)); //分钟
        endpoints.tokenServices(tokenServices);
    }


    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.checkTokenAccess("isAuthenticated()");
        oauthServer.allowFormAuthenticationForClients();
    }

    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
           clients.withClientDetails(clientDetails());
       /*       
        *        基于内存配置项
        *        clients.inMemory()
                .withClient("community")
                .secret("community")
                .authorizedGrantTypes("authorization_code").redirectUris("http://tech.taiji.com.cn/")
                .scopes("app").and() .withClient("dev")
                .secret("dev")
                .authorizedGrantTypes("authorization_code").redirectUris("http://localhost:7777/")
                .scopes("app");*/
    }
}

客户端应用主要配置如下:

application.properties中Oauth2配置,如下

security.oauth2.client.clientId=dev
security.oauth2.client.clientSecret=dev
security.oauth2.client.accessTokenUri=http://localhost:9999/oauth/token
security.oauth2.client.userAuthorizationUri=http://localhost:9999/oauth/authorize
security.oauth2.resource.loadBalanced=true
security.oauth2.resource.userInfoUri=http://localhost:9999/me
security.oauth2.resource.logout.url=http://localhost:9999/revoke-token
security.oauth2.default.roleName=ROLE_USER

Oauth2Config配置,授权Oauth2Sso配置和Spring Security配置,如下:

@Configuration
@EnableOAuth2Sso
public class Oauth2Config extends WebSecurityConfigurerAdapter{
	@Autowired
	CustomSsoLogoutHandler customSsoLogoutHandler;
	
	@Autowired
	OAuth2ClientContext oauth2ClientContext;
	
	@Bean
	public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
	    StrictHttpFirewall firewall = new StrictHttpFirewall();
	    firewall.setAllowUrlEncodedSlash(true);
	    firewall.setAllowSemicolon(true);
	    return firewall;
	}
	
	@Bean
	@ConfigurationProperties("security.oauth2.client")
	public AuthorizationCodeResourceDetails taiji() {
		return new AuthorizationCodeResourceDetails();
	}
	
	@Bean
	public CommunitySuccessHandler customSuccessHandler() {
		CommunitySuccessHandler customSuccessHandler = new CommunitySuccessHandler();
		customSuccessHandler.setDefaultTargetUrl("/");
		return customSuccessHandler;
	}
	
	@Bean
	public CustomFailureHandler customFailureHandler() {
		CustomFailureHandler customFailureHandler = new CustomFailureHandler();
		customFailureHandler.setDefaultFailureUrl("/index");
		return customFailureHandler;
	}
	
	@Bean
	@Primary
	@ConfigurationProperties("security.oauth2.resource")
	public ResourceServerProperties taijiOauthorResource() {
		return new ResourceServerProperties();
	}

	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		List authenticationProviderList = new ArrayList();
		authenticationProviderList.add(customAuthenticationProvider());
		AuthenticationManager authenticationManager = new ProviderManager(authenticationProviderList);
		return authenticationManager;
	}

	@Autowired
	public TaijiUserDetailServiceImpl userDetailsService;

	@Bean
	public TaijiAuthenticationProvider customAuthenticationProvider() {
		TaijiAuthenticationProvider customAuthenticationProvider = new TaijiAuthenticationProvider();
		customAuthenticationProvider.setUserDetailsService(userDetailsService);
		return customAuthenticationProvider;
	}


	@Autowired
	private MenuService menuService;
	@Autowired
	private RoleService roleService;
	@Bean
	public TaijiSecurityMetadataSource taijiSecurityMetadataSource() {
		TaijiSecurityMetadataSource fisMetadataSource = new TaijiSecurityMetadataSource();
//		fisMetadataSource.setMenuService(menuService);
		fisMetadataSource.setRoleService(roleService);
		return fisMetadataSource;
	}
	
	@Autowired
	private CommunityAccessDecisionManager accessDecisionManager;
	@Bean
	public CommunityFilterSecurityInterceptor communityfiltersecurityinterceptor() throws Exception {
		CommunityFilterSecurityInterceptor taijifiltersecurityinterceptor = new CommunityFilterSecurityInterceptor();
		taijifiltersecurityinterceptor.setFisMetadataSource(taijiSecurityMetadataSource());
		taijifiltersecurityinterceptor.setAccessDecisionManager(accessDecisionManager);
		taijifiltersecurityinterceptor.setAuthenticationManager(authenticationManagerBean());
		return taijifiltersecurityinterceptor;
	}
	
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
//		        .antMatchers("/").permitAll()  
//				.antMatchers("/login").permitAll() // 
//				.antMatchers("/image").permitAll() // 
//				.antMatchers("/upload/*").permitAll() // for
//				.antMatchers("/common/**").permitAll() // for 
//				.antMatchers("/community/**").permitAll()
				
//		        .antMatchers("/").anonymous()
		        .antMatchers("/personal/**").authenticated()
		        .antMatchers("/notify/**").authenticated()
		        .antMatchers("/admin/**").authenticated()
		        .antMatchers("/manage/**").authenticated()
		        .antMatchers("/**/personal/**").authenticated()
		        .antMatchers("/user/**").authenticated()
				.anyRequest()
				.permitAll()
//				.authenticated()
				.and()
				.logout()
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
				.addLogoutHandler(customSsoLogoutHandler)
				.deleteCookies("JSESSIONID").invalidateHttpSession(true)
				.and()
				.csrf().disable()
				//.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
				//.and()
				.addFilterBefore(loginFilter(), BasicAuthenticationFilter.class)
				.addFilterAfter(communityfiltersecurityinterceptor(), FilterSecurityInterceptor.class);///TaijiSecurity权限控制
	}
	
	@Override
	public void configure(WebSecurity web) throws Exception {
		// 解决静态资源被拦截的问题
		web.ignoring().antMatchers("/theme/**")
				.antMatchers("/community/**")
				.antMatchers("/common/**")
				.antMatchers("/upload/*");
		web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
	}

	
	public OAuth2ClientAuthenticationProcessingFilter loginFilter() throws Exception {
		OAuth2ClientAuthenticationProcessingFilter ff = new OAuth2ClientAuthenticationProcessingFilter("/login");
		OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(taiji(),oauth2ClientContext);
		ff.setRestTemplate(restTemplate);
		UserInfoTokenServices  tokenServices = new UserInfoTokenServices(taijiOauthorResource().getUserInfoUri(), taiji().getClientId());
		tokenServices.setRestTemplate(restTemplate);
		ff.setTokenServices(tokenServices);
		ff.setAuthenticationSuccessHandler(customSuccessHandler());
        ff.setAuthenticationFailureHandler(customFailureHandler());
		return ff;
	}
}

授权成功回调类,认证成功用户落地,如下:

public class CommunitySuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

	protected final Log logger = LogFactory.getLog(this.getClass());

	private RequestCache requestCache = new HttpSessionRequestCache();
	@Autowired
	private UserService userService;
	@Autowired
	private RoleService roleService;
	@Inject
	AuthenticationManager authenticationManager;
	@Value("${security.oauth2.default.roleName}")
	private String defaultRole;
	@Inject
	TaijiOperationLogService taijiOperationLogService;
	
	@Inject
	CommunityConfiguration communityConfiguration;
	
	@Inject
	private ObjectMapper objectMapper;

	@ScoreRule(code="login_score")
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws ServletException, IOException {
		// 存放authentication到SecurityContextHolder
		SecurityContextHolder.getContext().setAuthentication(authentication);
		HttpSession session = request.getSession(true);
		// 在session中存放security context,方便同一个session中控制用户的其他操作
		session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
		OAuth2Authentication oauth2Authentication = (OAuth2Authentication) authentication;
		Object details = oauth2Authentication.getUserAuthentication().getDetails();
		UserDto user = saveUser((Map) details);//用户落地
		Collection obtionedGrantedAuthorities = obtionGrantedAuthorities(user);
		UsernamePasswordAuthenticationToken newToken = new UsernamePasswordAuthenticationToken(
				new User(user.getLoginName(), "", true, true, true, true, obtionedGrantedAuthorities), 
				authentication.getCredentials(), obtionedGrantedAuthorities);
		
		newToken.setDetails(details);
		Object oath2details=oauth2Authentication.getDetails();
		oauth2Authentication = new OAuth2Authentication(oauth2Authentication.getOAuth2Request(), newToken);
		oauth2Authentication.setDetails(oath2details);
		oauth2Authentication.setAuthenticated(true);
		SecurityContextHolder.getContext().setAuthentication(oauth2Authentication);
		
		LogUtil.log2database(taijiOperationLogService, request, user.getLoginName(), "user", "", "", "user_login", "登录", "onAuthenticationSuccess","");
		session.setAttribute("user", user);
		Collection authorities = (Collection) authentication.getAuthorities();
		
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
			requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		clearAuthenticationAttributes(request);
		// Use the DefaultSavedRequest URL
		String targetUrl = savedRequest.getRedirectUrl();
//		logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);

		logger.debug("Redirecting to last savedRequest Url: " + targetUrl);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
//		getRedirectStrategy().sendRedirect(request, response, this.getDefaultTargetUrl());
	}

	public void setRequestCache(RequestCache requestCache) {
		this.requestCache = requestCache;
	}
	
	//用户落地
	private UserDto saveUser(Map userInfo) {
		UserDto dto=null;
		try {
			String json = objectMapper.writeValueAsString(userInfo);
			dto = objectMapper.readValue(json,UserDto.class);
		} catch (JsonProcessingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		UserDto user=userService.findByLoginName(dto.getLoginName());
		if(user!=null) {
			return user;
		}
		Set roles= new HashSet();
		RoleDto role = roleService.findByRoleName(defaultRole);
		roles.add(role);
		dto.setRoles(roles);
		List list = new ArrayList();
		list.add(dto);
		dto.generateTokenForCommunity(communityConfiguration.getControllerSalt());
		String id =userService.saveUserWithRole(dto,communityConfiguration.getControllerSalt());
		dto.setId(id);
		return dto;
	}
	
	/**
     * Map转成实体对象
     *
     * @param map   map实体对象包含属性
     * @param clazz 实体对象类型
     * @return
     */
    public static  T map2Object(Map map, Class clazz) {
        if (map == null) {
            return null;
        }
        T obj = null;
        try {
            obj = clazz.newInstance();
 
            Field[] fields = obj.getClass().getDeclaredFields();
            for (Field field : fields) {
                int mod = field.getModifiers();
                if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {
                    continue;
                }
                field.setAccessible(true);
                String filedTypeName = field.getType().getName();
                if (filedTypeName.equalsIgnoreCase("java.util.date")) {
                    String datetimestamp = String.valueOf(map.get(field.getName()));
                    if (datetimestamp.equalsIgnoreCase("null")) {
                        field.set(obj, null);
                    } else {
                        field.set(obj, new Date(Long.parseLong(datetimestamp)));
                    }
                } else {
                	String v = map.get(field.getName()).toString();
                    field.set(obj, map.get(field.getName()));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }
	
	

	// 取得用户的权限
	private Collection obtionGrantedAuthorities(UserDto users) {
		Collection authSet = new HashSet();
		// 获取用户角色
		Set roles = users.getRoles();
		if (null != roles && !roles.isEmpty())
			for (RoleDto role : roles) {
		authSet.add(new SimpleGrantedAuthority(role.getId()));
			}
		return authSet;
	}
}

客户端应用,单点登录方法,如下:

@RequestMapping(value = "/loadToken", method = { RequestMethod.GET })
	public void loadToken(Model model,HttpServletResponse response,@RequestParam(value = "clientId", required = false) String  clientId) {
		String token = "";
		RequestAttributes ra = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes sra = (ServletRequestAttributes) ra;
		HttpServletRequest request = sra.getRequest();
		HttpSession session = request.getSession();
		if (session.getAttribute("SPRING_SECURITY_CONTEXT") != null) {
			SecurityContext securityContext = (SecurityContext)session.getAttribute("SPRING_SECURITY_CONTEXT");
			Authentication authentication = securityContext.getAuthentication();
			OAuth2AuthenticationDetails OAuth2AuthenticationDetails = (OAuth2AuthenticationDetails) authentication.getDetails();
			token = OAuth2AuthenticationDetails.getTokenValue();
		}
		try {
			String url = "http://localhost:9999/rediect?clientId=dev&token="+token;
			response.sendRedirect(url);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

服务端应用,单点登录方法,如下:

@RequestMapping("/rediect")
	public String rediect(HttpServletResponse responsel, String clientId, String token) {
		OAuth2Authentication authentication = tokenStore.readAuthentication(token);
		if (authentication == null) {
			throw new InvalidTokenException("Invalid access token: " + token);
		}
		OAuth2Request request = authentication.getOAuth2Request();
		Map map = new HashMap();
		map.put("code", request.getRequestParameters().get("code"));
		map.put("grant_type", request.getRequestParameters().get("grant_type"));
		map.put("response_type", request.getRequestParameters().get("response_type"));
		//TODO 需要查询一下要跳转的Client_id配置的回调地址
		map.put("redirect_uri", "http://127.0.0.1:8888");
		map.put("client_id", clientId);
		map.put("state", request.getRequestParameters().get("state"));
		request = new OAuth2Request(map, clientId, request.getAuthorities(), request.isApproved(), request.getScope(),
				request.getResourceIds(), map.get("redirect_uri").toString(), request.getResponseTypes(),request.getExtensions()); // 模拟用户登录
		Authentication t = tokenStore.readAuthentication(token);
		OAuth2Authentication auth = new OAuth2Authentication(request, t);
		OAuth2AccessToken new_token = defaultTokenServices.createAccessToken(auth);
		return "redirect:/user_info?access_token=" + new_token.getValue();
	}
@RequestMapping({ "/user_info" })
	public void user(String access_token,HttpServletResponse response) {
		OAuth2Authentication auth=tokenStore.readAuthentication(access_token);
		OAuth2Request request=auth.getOAuth2Request();
	  Map map = new LinkedHashMap<>();
	  map.put("loginName", auth.getUserAuthentication().getName());
	  map.put("password", auth.getUserAuthentication().getName());
	  map.put("id", auth.getUserAuthentication().getName());
	  try {
		response.sendRedirect(request.getRedirectUri()+"?name="+auth.getUserAuthentication().getName());
	} catch (IOException e) {
		e.printStackTrace();
	}
}

个人总结

Oauth2的设计相对复杂,需要深入学习多看源码才能了解内部的一些规则,如数据token的存储是用的实体序列化后内容,需要反序列才能在项目是使用,也许是为了安全,但在学习过程需要提前掌握,还有在token的过期时间不能为0,通常来讲过期时间为0代表长期有效,但在Oauth2中则报错,这些坑需要一点点探索。

通过集成Spring Security和Oauth2较大的提供的开发的效率,也提供的代码的灵活性和可用性。但封装的核心类需要大家都了解一下,通读下代码,以便在项目中可随时获取需要的参数。

示例代码

以下是个人的一套代码,供参考。

基于Spring Cloud的微服务框架集成Oauth2的代码示例

Oauth2数据结构,如下:

表名 字段名 字段说明
oauth_client_details client_id 主键,必须唯一,不能为空. 
用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成). 
对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.
resource_ids 客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: "unity-resource,mobile-resource". 
该字段的值必须来源于与security.xml中标签‹oauth2:resource-server的属性resource-id值一致. 在security.xml配置有几个‹oauth2:resource-server标签, 则该字段可以使用几个该值. 
在实际应用中, 我们一般将资源进行分类,并分别配置对应的‹oauth2:resource-server,如订单资源配置一个‹oauth2:resource-server, 用户资源又配置一个‹oauth2:resource-server. 当注册客户端时,根据实际需要可选择资源id,也可根据不同的注册流程,赋予对应的资源id.
client_secret 用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成). 
对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念.
scope 指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔,如: "read,write". 
scope的值与security.xml中配置的‹intercept-urlaccess属性有关系. 如‹intercept-url的配置为
‹intercept-url pattern="/m/**" access="ROLE_MOBILE,SCOPE_READ"/>
则说明访问该URL时的客户端必须有read权限范围. write的配置值为SCOPE_WRITEtrust的配置值为SCOPE_TRUST
在实际应该中, 该值一般由服务端指定, 常用的值为read,write.
authorized_grant_types 指定客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔,如: "authorization_code,password". 
在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: "authorization_code,refresh_token"(针对通过浏览器访问的客户端); "password,refresh_token"(针对移动设备的客户端). 
implicitclient_credentials在实际中很少使用.
web_server_redirect_uri 客户端的重定向URI,可为空, 当grant_type为authorization_codeimplicit时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:
  • 当grant_type=authorization_code时, 第一步 从 spring-oauth-server获取 'code'时客户端发起请求时必须有redirect_uri参数, 该参数的值必须与web_server_redirect_uri的值一致. 第二步 用 'code' 换取 'access_token' 时客户也必须传递相同的redirect_uri
    在实际应用中, web_server_redirect_uri在注册时是必须填写的, 一般用来处理服务器返回的code, 验证state是否合法与通过code去换取access_token值. 
    在spring-oauth-client项目中, 可具体参考AuthorizationCodeController.java中的authorizationCodeCallback方法.
  • 当grant_type=implicit时通过redirect_uri的hash值来传递access_token值.如:
    http://localhost:7777/spring-oauth-client/implicit#access_token=dc891f4a-ac88-4ba6-8224-a2497e013865&token_type=bearer&expires_in=43199
    然后客户端通过JS等从hash值中取到access_token值.
authorities 指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔, 如: "ROLE_UNITY,ROLE_USER". 
对于是否要设置该字段的值,要根据不同的grant_type来判断, 若客户端在Oauth流程中需要用户的用户名(username)与密码(password)的(authorization_code,password), 
则该字段可以不需要设置值,因为服务端将根据用户在服务端所拥有的权限来判断是否有权限访问对应的API. 
但如果客户端在Oauth流程中不需要用户信息的(implicit,client_credentials), 
则该字段必须要设置对应的权限值, 因为服务端将根据该字段值的权限来判断是否有权限访问对应的API. 

(请在spring-oauth-client项目中来测试不同grant_type时authorities的变化)

access_token_validity 设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时). 
在服务端获取的access_token JSON数据中的expires_in字段的值即为当前access_token的有效时间值. 
在项目中, 可具体参考DefaultTokenServices.java中属性accessTokenValiditySeconds
在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.
refresh_token_validity 设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 
若客户端的grant_type不包括refresh_token,则不用关心该字段 在项目中, 可具体参考DefaultTokenServices.java中属性refreshTokenValiditySeconds

在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.
additional_information 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据,如:
{"country":"CN","country_code":"086"}
按照spring-security-oauth项目中对该字段的描述 
Additional information for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information. 
(详见ClientDetails.javagetAdditionalInformation()方法的注释)在实际应用中, 可以用该字段来存储关于客户端的一些其他信息,如客户端的国家,地区,注册时的IP地址等等.
create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
archived 用于标识客户端是否已存档(即实现逻辑删除),默认值为'0'(即未存档). 
对该字段的具体使用请参考CustomJdbcClientDetailsService.java,在该类中,扩展了在查询client_details的SQL加上archived = 0条件 (扩展字段)
trusted 设置客户端是否为受信任的,默认为'0'(即不受信任的,1为受信任的). 
该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为0,则会跳转到让用户Approve的页面让用户同意授权, 
若该字段为1,则在登录后不需要再让用户Approve同意授权(因为是受信任的). 
对该字段的具体使用请参考OauthUserApprovalHandler.java. (扩展字段)
autoapprove 设置用户是否自动Approval操作, 默认值为 'false', 可选值包括 'true','false', 'read','write'. 
该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为'true'或支持的scope值,则会跳过用户Approve的页面, 直接授权. 
该字段与 trusted 有类似的功能, 是 spring-security-oauth2 的 2.0 版本后添加的新属性.

 在项目中,主要操作oauth_client_details表的类是JdbcClientDetailsService.java, 更多的细节请参考该类. 
也可以根据实际的需要,去扩展或修改该类的实现.

oauth_client_token create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
token_id 从服务器端获取到的access_token的值.
token 这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进制数据.
authentication_id 该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 
具体实现请参考DefaultClientKeyGenerator.java类.
user_name 登录时的用户名
client_id  

 该表用于在客户端系统中存储从服务端获取的token数据, 在spring-oauth-server项目中未使用到. 
oauth_client_token表的主要操作在JdbcClientTokenServices.java类中, 更多的细节请参考该类.

oauth_access_token create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
token_id 该字段的值是将access_token的值通过MD5加密后存储的.
token 存储将OAuth2AccessToken.java对象序列化后的二进制数据, 是真实的AccessToken的数据值.
authentication_id 该字段具有唯一性, 其值是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultAuthenticationKeyGenerator.java类.
user_name 登录时的用户名, 若客户端没有用户名(如grant_type="client_credentials"),则该值等于client_id
client_id  
authentication 存储将OAuth2Authentication.java对象序列化后的二进制数据.
refresh_token 该字段的值是将refresh_token的值通过MD5加密后存储的.

 在项目中,主要操作oauth_access_token表的对象是JdbcTokenStore.java. 更多的细节请参考该类.

oauth_refresh_token create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
token_id 该字段的值是将refresh_token的值通过MD5加密后存储的.
token 存储将OAuth2RefreshToken.java对象序列化后的二进制数据.
authentication 存储将OAuth2Authentication.java对象序列化后的二进制数据.

 在项目中,主要操作oauth_refresh_token表的对象是JdbcTokenStore.java. (与操作oauth_access_token表的对象一样);更多的细节请参考该类. 
如果客户端的grant_type不支持refresh_token,则不会使用该表.

oauth_code create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
code 存储服务端系统生成的code的值(未加密).
authentication 存储将AuthorizationRequestHolder.java对象序列化后的二进制数据.

 在项目中,主要操作oauth_code表的对象是JdbcAuthorizationCodeServices.java. 更多的细节请参考该类. 
只有当grant_type为"authorization_code"时,该表中才会有数据产生; 其他的grant_type没有使用该表.

你可能感兴趣的:(Oauth2)