基于securtiy框架的单点登录配置详解
(需要澄清,这个教程是2年前的写的教程,版本是基于springboot 1.5.9的版本 )
但大致流程和最新的2.2.0+的版本变化不大。
目录
一、关于配置文件的编写规则
我这里使用的application.properties文件来配置,可自行转换为yml文件配置,按照固定格式转换配置语法即可,含义是一样的,两种文件之间语法转换的规则介绍如下。
例如application.properties中的以下配置:
security.oauth2.client.userAuthorizationUri=http://localhost:7780/oauth/authorize
在yml文件中对应的配置格式如下:
security:
oauth2:
client:
user-authorization-uri:http://localhost:7780/oauth/authorize
需要注意userAuthorizationUri和user-authorization-uri之间的转换,需要重大字母开始到下一个大写字母结尾的单词使用”-”符号进行连接即可。另外最后的赋值无论如何不要使用””,只需要填写实际需要赋的值即可。
二、认证授权端配置
认证授权服务端主要做三件事。
开启认证授权自动配置后,向外部提供换取TOKEN的相关接口。
主要配置代码如下
AuthorizationConfig.class
package ywcai.ls.oauth.config;
//省略了包导入代码,可自行根据IDE提示添加
@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("client")
// .secret("111111")
// .resourceIds("oauth")
.authorizedGrantTypes("authorization_code", "refresh_token")//设置验证方式
// .redirectUris("http://localhost/callback")//可指定也可以由客户端携带
.scopes("all")
.autoApprove(true);//默认登录后可直接授权
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore);
}
@Bean
public TokenStore tokenStore()
{
//这里为了简单达到目的,直接使用内存存储Token和用户信息。
return new InMemoryTokenStore();
}
}
认证服务器自身的安全控制
这个自定义WebSecurityConfigurerAdapter .class的配置也很重要,否则认证服务器的用户信息在哪里来?认证服务器任何人都可以直接访问获取到token吗?显然也不是,因此要对认证服务器本身提供code、token等相关接口进行安全控制。但要注意的是/user或/oauth/check_token的用于客户端获取认证信息的接口是通过token来验证,不能进行安全控制,否则客户端即使获取到token也永远无法进行验证并拿到认证用户的信息。
//以下代码使用了springSecurity默认的prodiver。因此除了注入MyUserDetailsService.class的用户信息外,无需再做其他配置
WebSecurityConfig.class
package ywcai.ls.oauth.config;
//同样去掉了包导入代码,可自行根据IDE提示导入
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsService myUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user**","/oauth/check_token**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
}
配置认证用户信息获取类
这里为了方便测试,直接在代码里建了一个用户名admin,密码111111,权限为BASE的用户。
UserDetailsService.class
package ywcai.ls.oauth.config;
@Service
@Qualifier(value="myUserDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String arg0) throws UsernameNotFoundException {
// TODO Auto-generated method stub
if ("admin".equals(arg0)) {
User user = createUser();
return user;
}
return null;
}
private User createUser() {
// TODO Auto-generated method stub
Collection<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("BASE"));//获取oauth资源权限
User user = new User("admin","111111",authorities);//111111
return user;
}
}
@Controller的配置
CoreController.class这里面只需要配置一个接口,但也是最重要的接口:/user,为第三方应用提供用户信息获取的接口。
可能因为不同版本的代码不一样原因,应用客户端的获取用户信息代码会有比较大的差异。前期笔者在网上查看的教程代码都是直接返回一个Principal对象。均验证,以下方法在为第三方提供认证时均不可行。
类似以下代码:
@RequestMapping("/user")
@ResponseBody
public Map<String, Object> user(Principal principal) {
Map<String, Object> map = new HashMap<>();
return principal;
}
}
//客户端装载用户信息的代码明确是获取的一个MAP对象,显然上面接口是错误的。
//或者
@RequestMapping("/user")
@ResponseBody
public Map<String, Object> user(Principal principal) {
map.put("user", principal);
return map;
}
}
但实际上,客户端携带token来换取用户信息时, principal是始终为空的 。这样会造成客户端无法获取到用户信息,也无法做权限控制,更无法进一步进行业务操作。因此这上面端代码也是不可行的。但客户端会默认生成一个具有ROLE_USER权限的用户,所以看起来像是认证成功了。
经笔者多次研读源代码分析后,验证正确的用户认证信息获取接口代码如下:
package ywcai.ls.oauth.config;
@Controller
public class CoreController<T> {
@Autowired
private TokenStore tokenStore;
@RequestMapping("/user")
@ResponseBody
public Map<String, Object> user(@RequestHeader String authorization) {
//必须通过客户端{携带的token在服务端的token存储中获取用户信息。
//header中 Authorization传过来的格式为[type token]的格式
//因此必须先对Authorization传过来的数据进行分隔authorization.split(" ")[1]才是真正的token
Map<String, Object> map = new HashMap<>();
OAuth2Authentication authen=null;
try
{
authen=tokenStore.readAuthentication(authorization.split(" ")[1]);
if(authen==null)
{
map.put("error", "invalid token !");
return map;
}
}
catch(Exception e)
{
System.out.println(e);
map.put("error", e);
return map;
}
//注意这两个key都不能随便填,都是和客户端进行数据处理时进行对应的。
map.put("user", authen.getPrincipal());
map.put("authorities", authen.getAuthorities());
return map;
}
}
至此,认证授权服务端的核心代码配置完成。直接用框架生成的/oauth/check_token返回的结果在客户端调用时无法适配,估计是我选择的SpringSecurity和SpringSecurityOauth之间版本不一致,两边接口返回数据有些差异造成的,索性自己根据客户端所需的数据重写服务端接口。
最后再贴下服务端的application.properties配置
当然最后的springcloud微服务相关配置非必须配置。
security.basic.enabled=true
server.session.cookie.name= SERVERSESSION
server.port=7780
spring.application.name=ssoinf
eureka.instance.hostname=localhost
eureka.client.serviceUrl.defaultZone=http://localhost:7771/eureka
三、资源服务端配置
资源服务端配置也比较简单,贴出源码,不在过多阐述
首先在配置类上开启@EnableResourceServer注解
package ywcai.ls.mobileutil.config;
@Configuration
@EnableResourceServer
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
//什么也不干,使用默认配置就可以了
}
配置application.properties
security.basic.enabled=false
security.oauth2.resource.id= productinf
//非常重要,不要配错了,请求认证服务器验证身份的接口
security.oauth2.resource.userInfoUri= http://localhost:7780/user
security.oauth2.resource.preferTokenInfo= false
server.port=7779
spring.application.name=productinf
eureka.instance.hostname=localhost
eureka.client.serviceUrl.defaultZone=http://localhost:7771/eureka
用于测试的@Controller资源
CoreController.class
package ywcai.ls.mobileutil.controller;
@RestController
public class CoreController {
@RequestMapping(value="/test",method=RequestMethod.GET)
String test()
{
System.out.println("run the test");
return "SUCCESS";
}
}
非常简单,资源服务器就配置完了,如果对资源服务器也要做不同角色和权限的分级控制,同下面WEB应用客户端配置一样,笔者会在下面介绍权限控制。
四、WEB应用客户端配置
客户端配置关键
在于@EnableOAuth2Sso注解的标注和属性文件的配置
启动类上加@EnableOAuth2Sso同时也不配置WebSecurityConfigurerAdapter类,则代表可用框架的默认配置处理所有事务。但如果完全使用默认配置目前笔者发现有如下的问题。
a 默认配置不能对本地应用进行权限控制
b 默认配置是不允许客户端加载frame的
因此,根据实际使用的场景,一般情况都需要对WebSecurityConfigurerAdapter做一些自定义设置。
如果既要大量使用@EnableOAuth2Sso注解引用的默认配置,但又要修改配置中某些配置进行自定义,则需要新建WebSecurityConfigurerAdapter进行自定义部分重写。此时需要注意@EnableOAuth2Sso必须在新建的WebSecurityConfigurerAdapter配置文件上进行注解。否则WebSecurityConfigurerAdapter的配置会覆盖@EnableOAuth2Sso的认证链配置。原因是为什么呢?仔细理解下源码就知道了。
EnableOAuth2Sso注解源代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties(OAuth2SsoProperties.class)
@Import({ OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class,
ResourceServerTokenServicesConfiguration.class })
public @interface EnableOAuth2Sso {
}
可以看出,其中引入了两个配置类:
OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class。很明显从命名可以看出来一个是默认配置类,一个是自定义配置类。
看看两个类的主要代码
OAuth2SsoDefaultConfiguration.class代码
@Configuration
@Conditional(NeedsWebSecurityCondition.class)
public class OAuth2SsoDefaultConfiguration extends WebSecurityConfigurerAdapter
implements Ordered {
private final ApplicationContext applicationContext;
private final OAuth2SsoProperties sso;
public OAuth2SsoDefaultConfiguration(ApplicationContext applicationContext,
OAuth2SsoProperties sso) {
this.applicationContext = applicationContext;
this.sso = sso;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests().anyRequest().authenticated();
new SsoSecurityConfigurer(this.applicationContext).configure(http);
}
@Override
public int getOrder() {
if (this.sso.getFilterOrder() != null) {
return this.sso.getFilterOrder();
}
if (ClassUtils.isPresent(
"org.springframework.boot.actuate.autoconfigure.ManagementServerProperties",
null)) {
// If > BASIC_AUTH_ORDER then the existing rules for the actuator
// endpoints will take precedence. This value is < BASIC_AUTH_ORDER.
return SecurityProperties.ACCESS_OVERRIDE_ORDER - 5;
}
return SecurityProperties.ACCESS_OVERRIDE_ORDER;
}
protected static class NeedsWebSecurityCondition extends EnableOAuth2SsoCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
return ConditionOutcome.inverse(super.getMatchOutcome(context, metadata));
}
}
}
其中条件注解@Conditional(NeedsWebSecurityCondition.class)表明当NeedsWebSecurityCondition满足时该条件成立,客户端使用该类作为默认配置。
而NeedsWebSecurityCondition是继承自EnableOAuth2SsoCondition,但对EnableOAuth2SsoConditionde的条件判断进行了取反操作。参考
return ConditionOutcome.inverse(super.getMatchOutcome(context, metadata))
这段代码
再看看OAuth2SsoCustomConfiguration.class配置类的注解
@Configuration
@Conditional(EnableOAuth2SsoCondition.class)
public class OAuth2SsoCustomConfiguration
implements ImportAware, BeanPostProcessor, ApplicationContextAware {
private Class<?> configType;
private ApplicationContext applicationContext;
……
}
当EnableOAuth2SsoCondition这个条件类为true时生效。
再看看EnableOAuth2SsoCondition.class条件类
class EnableOAuth2SsoCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
String[] enablers = context.getBeanFactory()
.getBeanNamesForAnnotation(EnableOAuth2Sso.class);
ConditionMessage.Builder message = ConditionMessage
.forCondition("@EnableOAuth2Sso Condition");
for (String name : enablers) {
if (context.getBeanFactory().isTypeMatch(name,
WebSecurityConfigurerAdapter.class)) {
return ConditionOutcome.match(message.found(
"@EnableOAuth2Sso annotation on WebSecurityConfigurerAdapter")
.items(name));
}
}
return ConditionOutcome.noMatch(message.didNotFind(
"@EnableOAuth2Sso annotation " + "on any WebSecurityConfigurerAdapter")
.atAll());
}
这个类里是有具体的判断实现:
for (String name : enablers) {
if (context.getBeanFactory().isTypeMatch(name,
WebSecurityConfigurerAdapter.class)) {
return ConditionOutcome.match(message.found(
"@EnableOAuth2Sso annotation on WebSecurityConfigurerAdapter")
.items(name));
}
}
判断WebSecurityConfigurerAdapter的类上是否带 @EnableOAuth2Sso注解时。
当WebSecurityConfigurerAdapter中加入@EnableOAuth2Sso注解,这个配置最后生成的过滤器链中会加入 oauth2 的过滤器 OAuth2ClientAuthenticationProcessingFilter。具体代码在OAuth2SsoCustomConfiguration.class中拦截后做的动态代理。
private static class SsoSecurityAdapter implements MethodInterceptor {
private SsoSecurityConfigurer configurer;
SsoSecurityAdapter(ApplicationContext applicationContext) {
this.configurer = new SsoSecurityConfigurer(applicationContext);
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (invocation.getMethod().getName().equals("init")) {
Method method = ReflectionUtils
.findMethod(WebSecurityConfigurerAdapter.class, "getHttp");
ReflectionUtils.makeAccessible(method);
HttpSecurity http = (HttpSecurity) ReflectionUtils.invokeMethod(method,
invocation.getThis());
this.configurer.configure(http);
}
return invocation.proceed();
}
}
如果WebSecurityConfigurerAdapter类没有带@EnableOAuth2Sso注解,则NeedsWebSecurityCondition条件成立,OAuth2SsoCustomConfiguration.class不会生效。
而OAuth2SsoDefaultConfiguration则是直接继承的WebSecurityConfigurerAdapter.class,并生成了默认配置。当客户端再次继承WebSecurityConfigurerAdapter.class后则会造成对原有默认配置的重写,原框架的配置无法再生效。
认证信息在客户端的抽取和装载流程
当服务端完成授权认证完,客户端获取到token后,根据客户端的配置,
security.oauth2.resource.userInfoUri=http://localhost:7780/user
security.oauth2.resource.tokenInfoUri = http://localhost:7780/oauth/check_token
会自动向服务端的userInfoUri或者tokenInfoUri路径请求获取认证对象的信息。具体访问路径则是根据在application.properties文件中的security.oauth2.resource.preferTokenInfo= false配置来决定。当制的为false时,优先访问userInfoUri,反之则访问tokenInfoUri,这里配置访问userInfoUri。
客户端调用授权服务端/user路径后,会返回一个MAP对象,而服务端在MAP中PUT了一个KEY为user的Principal对象,既登录用户的相关信息。在这里,服务端为什么put的KEY为”user”呢?且看客户端处理返回MAP对象的代码。
UserInfoTokenServices.class核心代码
package org.springframework.boot.autoconfigure.security.oauth2.resource;
public class UserInfoTokenServices implements ResourceServerTokenServices {
//一个用户数据适配器,解析抽取服务端返回的MAP
private PrincipalExtractor principalExtractor = new FixedPrincipalExtractor();
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
//调用getMap访问服务端
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if (map.containsKey("error")) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("userinfo returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
//通过extractAuthentication抽取Map中的用户信息对象
return extractAuthentication(map);
}
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
//调用了本地方法,
Object principal = getPrincipal(map);
List<GrantedAuthority> authorities = this.authoritiesExtractor
.extractAuthorities(map);
OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null,
null, null, null, null);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
protected Object getPrincipal(Map<String, Object> map) {
//调用了principalExtractor对象的数据抽取方法,principalExtractor代码见下面FixedPrincipalExtractor.class类的具体实现
Object principal = this.principalExtractor.extractPrincipal(map);
return (principal != null ? principal : "unknown");
}
@SuppressWarnings({ "unchecked" })
private Map<String, Object> getMap(String path, String accessToken) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting user info from: " + path);
}
try {
OAuth2RestOperations restTemplate = this.restTemplate;
if (restTemplate == null) {
BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
resource.setClientId(this.clientId);
restTemplate = new OAuth2RestTemplate(resource);
}
OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext()
.getAccessToken();
if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
accessToken);
token.setTokenType(this.tokenType);
restTemplate.getOAuth2ClientContext().setAccessToken(token);
}
return restTemplate.getForEntity(path, Map.class).getBody();
//具体的访问请求。
}
catch (Exception ex) {
this.logger.warn("Could not fetch user details: " + ex.getClass() + ", "
+ ex.getMessage());
return Collections.<String, Object>singletonMap("error",
"Could not fetch user details");
}
}
}
在org.springframework.boot.autoconfigure.security.oauth2.resource包下,
通过FixedPrincipalExtractor.class类对返回的MAP进行处理。
MAP抽取逻为遍历默认得静态数组所包含的所有key,因此服务端put到MAP的用户数据,关键字只要是下面的PRINCIPAL_KEYS数组中任意值即可。
FixedPrincipalExtractor.class
public class FixedPrincipalExtractor implements PrincipalExtractor {
private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username",
"userid", "user_id", "login", "id", "name" };
@Override
public Object extractPrincipal(Map<String, Object> map) {
for (String key : PRINCIPAL_KEYS) {
if (map.containsKey(key)) {
return map.get(key);
}
}
return null;
}
}
最后将获取到的OAuth2Authentication对象返回给OAuth2ClientAuthenticationProcessingFilter,注入到SecurityContext中,完整整个认证流程。
客户端配置具体如下
application.properties
#不允许默认的弹窗表单
security.basic.enabled=false
#允许页面加载frame
security.headers.frame=true
#不允许页面缓存
spring.thymeleaf.cache=false
#要拦截的路径,不配置则默认所有路径均拦截
#security.basic.path=/**
#设置登录和获取code默认回调地址,不配置则默认为/login
#security.oauth2.sso.login-path=/portal
#认证服务器分配ID
security.oauth2.client.clientId=client
#秘钥,可以不设置
#security.oauth2.client.clientSecret=111111
#认证服务器授权方式
security.oauth2.client.authorized-grant-types= authorization_code,refresh_token
#认证服务器获取token路径
security.oauth2.client.accessTokenUri=http://localhost:7780/oauth/token
#认证服务器获取CODE路径
security.oauth2.client.userAuthorizationUri=http://localhost:7780/oauth/authorize
#可以自定义回调路径,不设置则默认与登录路径security.oauth2.sso.login-path一致
#security.oauth2.client.preEstablishedRedirectUri=http://localhost/callback
#是否启用默认路径,如果要使用自定义路径,这里必须配置为false
security.oauth2.client.useCurrentUri= true
#获取到token后,token装配在http协议的header还是query/form中
security.oauth2.client.clientAuthenticationScheme=header
#获取到token后,获取用户认证信息的路径
security.oauth2.resource.userInfoUri=http://localhost:7780/user
#设置SESSION名称,主要避免多个资源端在同一服务器造成SESSION命名重叠
server.session.cookie.name= UISESSION
#------------------以下是微服务的配置项,非必须配置内容---------------------------------
#该应用的端口地址
server.port=80
#该应用在服务中心注册的服务名称
spring.application.name=manageinf
#该应用在服务中心注册的主机名称
eureka.instance.hostname=localhost
#微服务架构服务中心的地址
eureka.client.serviceUrl.defaultZone=http://localhost:7771/eureka
WebSecurityConfig.class
package ywcai.ls.controller;
//自行导入所需依赖包
@Configuration
@EnableOAuth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true)//如果要做本地权限控制,必须加这条注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
//必须注入这个bean,配合@EnableGlobalMethodSecurity注解控制用户访问权限
@Autowired
AuthenticationManager authenticationManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
super.configure(http);
http.headers().frameOptions().disable();//允许WEB的frame框架访问。
}
}
CoreController.class
package ywcai.ls.controller;
//自行导入所需依赖包
@Controller
public class CoreController {
//测试页面,完成认证后即可访问
@RequestMapping(value="/index",method=RequestMethod.GET)
String index() {
System.out.println("index");
return "/index";
}
//测试页面,即使完成认证,也仅允许权限中含“ADMIN”的用户访问
@RequestMapping(value="/test",method=RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasAuthority(ADMIN)")
String test() {
System.out.println("test");
return "xxx";
}
@RequestMapping(value="/userinfo",method=RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasAuthority('BASE')")
String getUserInfo() {
return "sss" ;
}
//自定义错误页面访问路径,无需对路径进行权限配置,框架可自行处理错误页面的访问权限
@RequestMapping(value="/err/{code}",method=RequestMethod.GET)
String ERR400(@PathVariable String code) {
String path="/errpage/"+code;
System.out.println(path);
return path+"";
}
}
//自定义错误页面路径类,SSO非必须,可沿用系统默认错误页面
//此处完成配置后的错误路径,无需单独进行权限配置,系统可访问
ErrorPageConfig.class
package ywcai.ls.controller;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
@Configuration
public class ErrorPageConfig {
@Bean
public EmbeddedServletContainerCustomizer costomizer()
{
return new EmbeddedServletContainerCustomizer(){
@Override
public void customize(ConfigurableEmbeddedServletContainer contaners) {
// TODO Auto-generated method stub
contaners.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST , "/err/400"));
contaners.addErrorPages(new ErrorPage(HttpStatus.FORBIDDEN , "/err/403"));
contaners.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR , "/err/500"));
contaners.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND , "/err/404"));
contaners.addErrorPages(new ErrorPage(HttpStatus.METHOD_NOT_ALLOWED , "/err/405"));
}};
}
}
启动类,需要注意的是,所有配置类均需要在启动类同一包或子包内,否则框架无法扫描到配置
ManageApplication.class
package ywcai.ls.controller;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ManageApplication {
public static void main(String[] args) {
SpringApplication.run(ManageApplication.class, args);
}
}
五、测试验证搭建效果
好了,SSO就是如此简单,几乎不用自己写什么代码。接下来看看效果吧!
先访问客户端首页地址localhost/index
携带相关认证数据被重定向到了认证服务器的/oauth/authorize页面。
此时由于认证服务器的/oauth/authorize同样开启了安全认证保护,因此又被重定向到了认证服务器的登录页面http://locahost:7780/login,要求进行登录认证。
输入账号密码登录
输入账号密码登录成功后会直接定向到/oauth/authorize页面,/oauth/authorize校验clientid、秘钥等信息后,又回调到客户端登录页面。客户端回调页面获取到code后会再次向服务端/oauth/token来换取Oauth2AccessToken。当客户端换取token成功后,会根据配置封装向服务端的/user连接获取用户信息的请求并返回一个MAP对象 。这里封装到服务端的/user路径请求是源码写死的,无法再在外部进行配置,最终获取用户信息成功后会在客户端应用进行装载。
访问test接口
当访问test接口时,需要ADMIN权限,由于服务端创建用户添加BASE权限,因此仍然无法访问
访问user接口
需要BASE权限,因此可以访问
测试调用资源接口
需要自行将客户端token写到header中,或者作为参数”?access_token=xxx”传入url,也可以重写接口统一传递处理token。
@RequestMapping(value="/test",method=RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasRole('USER')")
String test(Principal principal) {
System.out.println("test");
OAuth2AuthenticationDetails myToken = (OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();
String result=productinf.test(myToken.getTokenValue());
return result;
}
使用feigin编写productinf接口
@Component
@FeignClient(name="productinf")
public interface ProductInf {
@RequestMapping(value="/test",method=RequestMethod.GET)
String test(@RequestParam(value="access_token") String xxx) ;
}
结束语
基本将网上的所有教程、详解都搜索了一个遍,基本翻来覆去都是那几篇文章粘贴来粘贴去。基本上所有配置完全照搬仍然不能达到想要的需求,很多教程可能都没有被验证过是否真的有效。
花了一周末的时间完成了第三方认证授权和分权限控制的测试平台,而且服务端、资源端、客户端完全分离配置,独立运行。配置搭建环境的过程中有很多坑,在网上胡拼乱凑肯定是凑不出来的,最后仔细阅读了大量spring-security-oauth2的源码后终于穿越了整个配置环境,还包括涉及spring-boot-autoconfigure-security包中的UserInfoTokenServices类,同时也加入了spring cloud的相关配置和应用。
下一篇教程将注重介绍spring cloud的基础环境搭建和配置使用。