最近在写提供给第三方的登录和获取资源,老大说要使用oauth2 协议,因此最近在啃这一块。一下是个人的配置。
首先要理解什么是OAUTH2协议。
以下是阮一峰大大的文章:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html。
其实在看过这篇文章之后我还是有点云里雾里的,虽然明白了里面的运行机制,但是让自己去写,还是有点困难,因此在一直继续找。
以下是参考过的文章:
http://www.leftso.com/blog/139.html
https://github.com/leftso/demo-spring-boot-security-oauth2
http://conkeyn.iteye.com/blog/2296406
http://baijiahao.baidu.com/s?id=1573372009481203
http://jinnianshilongnian.iteye.com/blog/2038646
下面直接上代码:
首先是POM.xml文件依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth2.0 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
再然后就是三个配置文件
继承WebSecurityConfigurerAdapter 类的
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.zhibo.xmt.api.userinfo.service.IUserService;
import com.zhibo.xmt.common.vo.AuthUserVo;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 查询用户使用
@Autowired
private IUserService userService;//这个service要自己去实现
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication()
// .withUser("user").password("password").roles("USER")
// .and()
// .withUser("app_client").password("nopass").roles("USER")
// .and()
// .withUser("admin").password("password").roles("ADMIN");
//配置用户来源于数据库
/**
* 通过AuthenticationManagerBuilder的userDetailsService方法,设置进去就OK了。
* passwordEncoder方法设置的是用户密码的加密方式,这里设置的是MD5加密,
* 所以用户从前端登录时传过来的密码,在使用Security验证时会自动使用MD5加密。
*/
auth.userDetailsService(userDetailsService()).passwordEncoder(new Md5PasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated().and()
.httpBasic().and().csrf().disable();
// http.authorizeRequests()
// .antMatchers("/wap/**", "/api/**","/admin/**").permitAll()
// //其他地址的访问均需验证权限
// .anyRequest().authenticated()
// .and()
// .formLogin()
// //指定登录页是"/login"
// .loginPage("/resource/login")
// .defaultSuccessUrl("/resource/hello")//登录成功后默认跳转到"/hello"
// .permitAll()
// .and()
// .logout()
// .logoutSuccessUrl("/resource/home")//退出登录后的默认url是"/home"
// .permitAll();
/**
* //允许所有用户访问"/"和"/home"
http.authorizeRequests()
.antMatchers("/", "/home").permitAll()
//其他地址的访问均需验证权限
.anyRequest().authenticated()
.and()
.formLogin()
//指定登录页是"/login"
.loginPage("/login")
.defaultSuccessUrl("/hello")//登录成功后默认跳转到"/hello"
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/home")//退出登录后的默认url是"/home"
.permitAll();
*/
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
// 通过用户名获取用户信息
AuthUserVo account = userService.getAuthUserInfoByMobile(name);
if (account != null) {
System.out.println("account--->" + account.toString());
// 创建spring security安全用户
User user = new User(account.getMobile(), account.getPassword(),
AuthorityUtils.createAuthorityList(account.getRoles()));
System.out.println("user--->" + user.toString());
return user;
} else {
throw new UsernameNotFoundException("用户[" + name + "]不存在");
}
}
};
}
}
继承AuthorizationServerConfigurerAdapter 类的
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* 认证授权服务端
* @author Bruce
*
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Value("${resource.id:spring-boot-application}") // 默认值spring-boot-application
private String resourceId;
@Value("${access_token.validity_period:360}") // 默认值3600
int accessTokenValiditySeconds = 360;
@Value("${access_token.validity_period:3600}")
int refreshTokenValiditySeconds = 3600;// 30 days 2592000
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(this.authenticationManager);
endpoints.accessTokenConverter(accessTokenConverter());
endpoints.tokenStore(tokenStore());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')");
oauthServer.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("normal-app")//客户端ID
.authorizedGrantTypes("authorization_code", "implicit")
.authorities("ROLE_CLIENT")
.scopes("read")//授权用户的操作权限
.resourceIds(resourceId)
.accessTokenValiditySeconds(accessTokenValiditySeconds)//token有效期为120秒
.refreshTokenValiditySeconds(refreshTokenValiditySeconds) // 30 days
.and()
.withClient("trusted-app")//客户端ID
.authorizedGrantTypes("client_credentials", "password")
.authorities("ROLE_TRUSTED_CLIENT")
.scopes("read")//授权用户的操作权限
.resourceIds(resourceId)
.accessTokenValiditySeconds(accessTokenValiditySeconds)//token有效期为120秒
.refreshTokenValiditySeconds(refreshTokenValiditySeconds) // 30 days
.secret("secret");//密码
/**
* // 定义了客户端细节服务
clients
.inMemory()
.withClient("adminClient")
.authorizedGrantTypes("password", "refresh_token")
.authorities("ADMIN")
.scopes("admin", "read", "write")
.resourceIds(RESOURCE_ID_ADMIN)
.secret("12345")
.accessTokenValiditySeconds(3600) // 1 hour
.refreshTokenValiditySeconds(2592000) // 30 days
.and()
.withClient("apiClient")
.authorizedGrantTypes("password", "refresh_token")
.authorities("USER")
.scopes("api", "read", "write")
.resourceIds(RESOURCE_ID_API)
.secret("12345")
.accessTokenValiditySeconds(3600) // 1 hour
.refreshTokenValiditySeconds(2592000) // 30 days
;
*/
/**
* clients.inMemory()
.withClient("my-trusted-client")//客户端ID
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write", "trust")//授权用户的操作权限
.secret("secret")//密码
.accessTokenValiditySeconds(6000);//token有效期为120秒
*/
}
/**
* token converter
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
/***
* 重写增强token方法,用于自定义一些token返回的信息
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String userName = authentication.getUserAuthentication().getName();
User user = (User) authentication.getUserAuthentication().getPrincipal();// 与登录时候放进去的UserDetail实现类一直查看link{SecurityConfiguration}
/** 自定义一些token属性 ***/
final Map<String, Object> additionalInformation = new HashMap<>();
additionalInformation.put("userName", userName);
additionalInformation.put("roles", user.getAuthorities());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
return enhancedToken;
}
};
accessTokenConverter.setSigningKey("123");// 测试用,资源服务使用相同的字符达到一个对称加密的效果,生产时候使用RSA非对称加密方式
return accessTokenConverter;
}
/**
* token store
*
* @param accessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore() {
TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
return tokenStore;
}
}
继承ResourceServerConfigurerAdapter 类的
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* 资源服务
* @author Bruce
*
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Value("${resource.id:spring-boot-application}")
private String resourceId;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
// @formatter:off
resources.resourceId(resourceId);
// @formatter:on
}
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.requestMatcher(new OAuth2RequestedMatcher())
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
// @formatter:on
/**
* http.
anonymous().disable()
.requestMatchers().antMatchers("/user*\/**")
.and().authorizeRequests()
.antMatchers("/user*\/**").permitAll()
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
*/
}
/**
* 定义一个oauth2的请求匹配器
* @author leftso
*
*/
private static class OAuth2RequestedMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
String auth = request.getHeader("Authorization");
// //判断来源请求是否包含oauth2授权信息,这里授权信息来源可能是头部的Authorization值以Bearer开头,或者是请求参数中包含access_token参数,满足其中一个则匹配成功
// boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
boolean haveAccessToken = request.getParameter("access_token")!=null;
// return haveOauth2Token || haveAccessToken;
return haveAccessToken;
}
}
}
控制器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.zhibo.xmt.api.BaseController;
import com.zhibo.xmt.api.userinfo.service.IUserService;
import com.zhibo.xmt.common.vo.AuthUserVo;
/**
* 受保护的资源服务
* @author Bruce
*
*/
//@Controller
//@RequestMapping("/api/user")
@RestController
@RequestMapping("/resources")
public class AuthUserController extends BaseController {
@Autowired
private IUserService userService;
/**
*
* @param mobile
* @return
*/
@PreAuthorize("hasRole('ROLE_API_USER')")
@RequestMapping(value = "/getAuthUserInfoByMobile", method = RequestMethod.GET)
public @ResponseBody AuthUserVo getAuthUserInfoByMobile(@RequestParam(name = "userName",required = true)String mobile) {
AuthUserVo vo = userService.getAuthUserInfoByMobile(mobile);
return vo;
}
/**
* 需要用户角色权限
* @return
*/
@PreAuthorize("hasRole('ROLE_API_USER')")
@RequestMapping(value="/user", method=RequestMethod.GET)
public String helloUser() {
return "hello user1111";
}
/**
* 需要管理角色权限
*
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping(value="/admin", method=RequestMethod.GET)
public String helloAdmin() {
return "hello admin";
}
/**
* 需要客户端权限
*
* @return
*/
@PreAuthorize("hasRole('ROLE_CLIENT')")
@RequestMapping(value="/client", method=RequestMethod.GET)
public String helloClient() {
return "hello user authenticated by normal client";
}
/**
* 需要受信任的客户端权限
*
* @return
*/
@PreAuthorize("hasRole('ROLE_TRUSTED_CLIENT')")
@RequestMapping(value="/trusted_client", method=RequestMethod.GET)
public String helloTrustedClient() {
return "hello user authenticated by trusted client";
}
@RequestMapping(value="principal", method=RequestMethod.GET)
public Object getPrincipal() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return principal;
}
@RequestMapping(value="roles", method=RequestMethod.GET)
public Object getRoles() {
return SecurityContextHolder.getContext().getAuthentication().getAuthorities();
}
}
测试上面的编码
测试过程
步骤一:打开浏览器,输入地址
http://localhost:8080/oauth/authorize?client_id=normal-app&response_type=code&scope=read&redirect_uri=/resources/user
会提示输入用户名密码,这时候输入用户名leftso,密码111aaa将会出现以下界面
点击Authorize将获取一个随机的code,如图:
打开工具postmain,输入以下地址获取授权token
localhost:8080/oauth/token?code=r8YBUL&grant_type=authorization_code&client_id=normal-app&redirect_uri=/resources/user
注意:url中的code就是刚才浏览器获取的code值
获取的token信息如下图:
这时候拿到token就可以访问受保护的资源信息了,如下
localhost:8081//resources/user
首先,直接访问资源,会报错401如图:
我们加上前面获取的access token再试:
localhost:8081//resources/user?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3ByaW5nLWJvb3QtYXBwbGljYXRpb24iXSwidXNlcl9uYW1lIjoibGVmdHNvIiwic2NvcGUiOlsicmVhZCJdLCJyb2xlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn1dLCJleHAiOjE0OTEzNTkyMjksInVzZXJOYW1lIjoibGVmdHNvIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjgxNjI5NzQwLTRhZWQtNDM1Yy05MmM3LWZhOWIyODk5NmYzMiIsImNsaWVudF9pZCI6Im5vcm1hbC1hcHAifQ.YhDJkMSlyIN6uPfSFPbfRuufndvylRmuGkrdprUSJIM
这时候我们就能成功获取受保护的资源信息了:
到这里spring boot整合security oauth2 的基本使用已经讲解完毕.
扩展思维
留下一些扩展
1.认证服务的客户端信息是存放内存的,实际应用肯定是不会放内存的,考虑数据库,默认有个DataSource的方式,还有一个自己实现clientDetail接口方式
2.jwt这里测试用的最简单的对称加密,实际应用中使用的一般都是RSA非对称加密方式
原来文章地址:
http://www.leftso.com/blog/139.html
原GITHUB地址:
https://github.com/leftso/demo-spring-boot-security-oauth2