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

网友投稿 309 2022-12-28


基于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的存储是用的实体序列化后内容,需要反序列才能在项目是使http://用,也许是为了安全,但在学习过程需要提前掌握,还有在token的过期时间不能为0,通常来讲过期时间为0代表长期有效,但在Oauth2中则报错,这些坑需要一点点探索。

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

示例代码

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

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

Oauth2数据结构,如下:


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:网页版的接口测试工具在哪(网页版的接口测试工具在哪里)
下一篇:java 获取当前时间的三种方法
相关文章

 发表评论

暂时没有评论,来抢沙发吧~