首页 > 编程 > Java > 正文

基于Spring Security的Oauth2授权实现方法

2019-11-26 08:37:50
字体:
来源:转载
供稿:网友

前言

经过一段时间的学习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依赖如下

    <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId> </dependency>  <dependency>  <groupId>com.fasterxml.jackson.datatype</groupId>  <artifactId>jackson-datatype-joda</artifactId> </dependency> <dependency>  <groupId>org.thymeleaf.extras</groupId>  <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency> <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency>  <groupId>org.springframework.security.oauth</groupId>  <artifactId>spring-security-oauth2</artifactId> </dependency>  <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency>  <groupId>mysql</groupId>  <artifactId>mysql-connector-java</artifactId> </dependency> <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>

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 indexescreate 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 DATAINSERT 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)@EnableAuthorizationServerpublic 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@EnableResourceServerprotected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {    @Override    public void configure(HttpSecurity http) throws Exception {      http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();    }  }

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

@Configurationpublic 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=devsecurity.oauth2.client.clientSecret=devsecurity.oauth2.client.accessTokenUri=http://localhost:9999/oauth/tokensecurity.oauth2.client.userAuthorizationUri=http://localhost:9999/oauth/authorizesecurity.oauth2.resource.loadBalanced=truesecurity.oauth2.resource.userInfoUri=http://localhost:9999/mesecurity.oauth2.resource.logout.url=http://localhost:9999/revoke-tokensecurity.oauth2.default.roleName=ROLE_USER

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

@Configuration@EnableOAuth2Ssopublic 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<AuthenticationProvider> authenticationProviderList = new ArrayList<AuthenticationProvider>(); 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<GrantedAuthority> 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<GrantedAuthority> authorities = (Collection<GrantedAuthority>) 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<RoleDto> roles= new HashSet<RoleDto>(); RoleDto role = roleService.findByRoleName(defaultRole); roles.add(role); dto.setRoles(roles); List<UserDto> list = new ArrayList<UserDto>(); 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> T map2Object(Map<String, Object> map, Class<T> 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<GrantedAuthority> obtionGrantedAuthorities(UserDto users) { Collection<GrantedAuthority> authSet = new HashSet<GrantedAuthority>(); // 获取用户角色 Set<RoleDto> 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<String, String> 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数据结构,如下:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表