這篇文章主要介紹了基於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依賴如下
<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 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<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數據結構,如下:
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。