Springboot入门系列教程(6)-基于securtiy框架的单点登录配置详解

基于securtiy框架的单点登录配置详解

(需要澄清,这个教程是2年前的写的教程,版本是基于springboot 1.5.9的版本 )

但大致流程和最新的2.2.0+的版本变化不大。

 

目录

一、关于配置文件的编写规则

二、认证授权端配置

三、资源服务端配置

四、WEB应用客户端配置

五、测试验证搭建效果

结束语



一、关于配置文件的编写规则

我这里使用的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的基础环境搭建和配置使用。

 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值