SSO(Single Sign On)即单点登录,效果是多个系统间,只要登录了其中一个系统,别的系统不用登录操作也能访问。比如在浏览器上同时打开天猫和淘宝页面,在天猫页面进行登录,然后回到淘宝页面刷新后会发现淘宝也已经是登录状态了。这节将介绍如何使用Spring Security OAuth2实现单点登录。

框架搭建

我们需要创建一个maven多模块项目,包含认证服务器和两个客户端。

新建一个maven项目,作为项目的父模块,pom如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <packaging>pom</packaging>
  <modules>
      <module>sso-application-one</module>
      <module>sso-application-two</module>
      <module>sso-server</module>
  </modules>
  <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.1.6.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>cc.mrbird</groupId>
  <artifactId>sso</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>sso</name>
  <description>Demo project for Spring Boot</description>

  <properties>
      <java.version>1.8</java.version>
      <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
  </properties>

  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-oauth2</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-security</artifactId>
      </dependency>
      <dependency>
          <groupId>org.apache.commons</groupId>
          <artifactId>commons-lang3</artifactId>
      </dependency>
  </dependencies>

  <dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${spring-cloud.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
  </dependencyManagement>

  <build>
      <plugins>
          <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
      </plugins>
  </build>
</project>

 

然后在该maven项目下新建一个module,artifactId为sso-server(作为认证服务器),pom如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
      <artifactId>sso</artifactId>
      <groupId>cc.mrbird</groupId>
      <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>sso-server</artifactId>
</project>

 

接着继续新增一个module模块,artifactId为sso-application-one(作为客户端一),pom如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
      <artifactId>sso</artifactId>
      <groupId>cc.mrbird</groupId>
      <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>sso-application-one</artifactId>
</project>

 

另外一个客户端和sso-application-one一致,只不过artifactId为sso-application-two。

至此,项目的基本框架搭建好了,结构如下所示:

认证服务器配置

认证服务器作用就是作为统一令牌发放并校验的地方,所以我们先要编写一些基本的Spring Security 安全配置的代码指定如何进行用户认证。

新建一个Spring Security配置类,继承WebSecurityConfigurerAdapter

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Bean
  public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
  }
  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http.formLogin()
          .and()
              .authorizeRequests()
              .anyRequest()
              .authenticated();
  }
}

 

上面简单配置了密码加密使用bcrypt方式,并且所有请求都需要认证,认证方式为Spring Security自带的登录页面认证(也可以根据前面教程来自定义登录页面,这里为了简单起见,就直接用自带的登录页了)。

接着需要定义一个自定义用户登录认证的服务:

@Configuration
public class UserDetailService implements UserDetailsService {

  @Autowired
  private PasswordEncoder passwordEncoder;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      MyUser user = new MyUser();
      user.setUserName(username);
      user.setPassword(this.passwordEncoder.encode("123456"));

      return new User(username, user.getPassword(), user.isEnabled(),
              user.isAccountNonExpired(), user.isCredentialsNonExpired(),
              user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("user:add"));
  }
}

 

基本逻辑是用户名随便写,密码为123456,并且拥有user:add权限。这些前面都介绍过了,就不再详细说明了。MyUser的代码如下:

public class MyUser implements Serializable {
  private static final long serialVersionUID = 3497935890426858541L;

  private String userName;

  private String password;

  private boolean accountNonExpired = true;

  private boolean accountNonLocked= true;

  private boolean credentialsNonExpired= true;

  private boolean enabled= true;
  // get,set略
}

 

接着开始编写认证服务器配置。

新建SsoAuthorizationServerConfig,继承AuthorizationServerConfigurerAdapter:

@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

  @Autowired
  private PasswordEncoder passwordEncoder;
  @Autowired
  private UserDetailService userDetailService;

  @Bean
  public TokenStore jwtTokenStore() {
      return new JwtTokenStore(jwtAccessTokenConverter());
  }

  @Bean
  public JwtAccessTokenConverter jwtAccessTokenConverter() {
      JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
      accessTokenConverter.setSigningKey("test_key");
      return accessTokenConverter;
  }

  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      // 见下方
  }

  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
      endpoints.tokenStore(jwtTokenStore())
              .accessTokenConverter(jwtAccessTokenConverter())
              .userDetailsService(userDetailService);
  }

  @Override
  public void configure(AuthorizationServerSecurityConfigurer security) {
      security.tokenKeyAccess("isAuthenticated()"); // 获取密钥需要身份认证
  }
}

 

Token使用JWT,这些配置都在前面几节OAuth2教程里介绍过了就不再赘述了,这里详细说下configure(ClientDetailsServiceConfigurer clients)的配置:

...
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  clients.inMemory()
          .withClient("app-a")
          .secret(passwordEncoder.encode("app-a-1234"))
          .authorizedGrantTypes("refresh_token","authorization_code")
          .accessTokenValiditySeconds(3600)
          .scopes("all");
      .and()
          .withClient("app-b")
          .secret(passwordEncoder.encode("app-b-1234"))
          .authorizedGrantTypes("refresh_token","authorization_code")
          .accessTokenValiditySeconds(7200)
          .scopes("all");
}
...

 

这里分配了两个客户端配置,分别为app-a和app-b,因为使用默认的Spring Security登录页面来进行认证,所以需要开启authorization_code类型认证支持。

认证服务器的application.yml配置如下:

server:
port: 8080
servlet:
  context-path: /server

 

认证服务器的搭建就告一段落了,接下来开始客户端代码编写。

客户端配置

两个客户端的代码基本一致,所以这里只介绍其中一个,另一个可以参考源码。

在客户端SpringBoot入口类上添加@EnableOAuth2Sso注解,开启SSO的支持:

@EnableOAuth2Sso
@SpringBootApplication
public class SsoApplicaitonOne {

  public static void main(String[] args) {
      new SpringApplicationBuilder(SsoApplicaitonOne.class).run(args);
  }
}

 

接下来的重点是配置文件application.yml的配置:

security:
oauth2:
  client:
    client-id: app-a
    client-secret: app-a-1234
    user-authorization-uri: http://127.0.0.1:8080/server/oauth/authorize
    access-token-uri: http://127.0.0.1:8080/server/oauth/token
  resource:
    jwt:
      key-uri: http://127.0.0.1:8080/server/oauth/token_key
server:
port: 9090
servlet:
  context-path: /app1

 

security.oauth2.client.client-idsecurity.oauth2.client.client-secret指定了客户端id和密码,这里和认证服务器里配置的client一致(另外一个客户端为app-b);user-authorization-uri指定为认证服务器的/oauth/authorize地址,access-token-uri指定为认证服务器的/oauth/token地址,jwt.key-uri指定为认证服务器的/oauth/token_key地址。

这里端口指定为9090,context-path为app1,另一个客户端端口指定为9091,context-path为app2。

接着在resources/static下新增一个index.html页面,用于跳转到另外一个客户端:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>管理系统一</title>
</head>
<body>
  <h1>管理系统一</h1>
  <a href="http://127.0.0.1:9091/app2/index.html">跳转到管理系统二</a>
</body>
</html>

 

为了验证是否认证成功,我们新增一个控制器:

@RestController
public class UserController {

  @GetMapping("user")
  public Principal user(Principal principal) {
      return principal;
  }
}

 

另外一个客户端的代码略,可以参考源码。

测试效果

先启动认证服务器,任何在启动两个客户端。启动后,访问http://127.0.0.1:9090/app1/index.html

可以看到页面被重定向到认证服务器的登录页面,根据我们定义的UserDetailService,用户名随便填,密码为123456。登录后页面跳转到:

这个URL为:http://127.0.0.1:8080/server/oauth/authorize?client_id=app-a&redirect_uri=http://127.0.0.1:9090/app1/login&response_type=code&state=7Dqcbr

页面提示的意思是:非法请求,至少需要一个重定向URL被注册到client。从URL中可以看出,redirect_uri为http://127.0.0.1:9090/app1/login,所以我们修改认证服务器client相关配置如下:

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  clients.inMemory()
          .withClient("app-a")
          .secret(passwordEncoder.encode("app-a-1234"))
          .authorizedGrantTypes("refresh_token","authorization_code")
          .accessTokenValiditySeconds(3600)
          .scopes("all")
          .redirectUris("http://127.0.0.1:9090/app1/login")
      .and()
          .withClient("app-b")
          .secret(passwordEncoder.encode("app-b-1234"))
          .authorizedGrantTypes("refresh_token","authorization_code")
          .accessTokenValiditySeconds(7200)
          .scopes("all")
          .redirectUris("http://127.0.0.1:9091/app2/login");
}

 

重启认证服务器,重复上面的过程,这次登录后,页面跳转到了授权页面:

点击Authorize:

这时候app-a对应的客户端已经登录了,点击跳转到系统二:

页面直接来到授权页,而不需要重新输入用户名密码,继续点击Authorize:

系统二也已经成功登录,访问http://127.0.0.1:9091/app2/user看是否能成功获取到用户信息:

到这里我们已经实现了单点登录的基本功能了。

但是在这个过程中需要用户点击Authorize授权,体验并不是很好,我们可以去掉它。修改认证服务器Client配置如下:

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  clients.inMemory()
          .withClient("app-a")
          .secret(passwordEncoder.encode("app-a-1234"))
          .authorizedGrantTypes("refresh_token","authorization_code")
          .accessTokenValiditySeconds(3600)
          .scopes("all")
          .autoApprove(true)
          .redirectUris("http://127.0.0.1:9090/app1/login")
      .and()
          .withClient("app-b")
          .secret(passwordEncoder.encode("app-b-1234"))
          .authorizedGrantTypes("refresh_token","authorization_code")
          .accessTokenValiditySeconds(7200)
          .scopes("all")
          .autoApprove(true)
          .redirectUris("http://127.0.0.1:9091/app2/login");
}

 

autoApprove(true)自动授权。修改后重启即可看到效果。

权限校验

Spring Security权限校验前面介绍过了,这里看下在单点登录模式下如何进行权限校验。

在客户端控制器里加入如下代码:

@GetMapping("auth/test1")
@PreAuthorize("hasAuthority('user:add')")
public String authTest1(){
  return "您拥有'user:add'权限";
}

@GetMapping("auth/test2")
@PreAuthorize("hasAuthority('user:update')")
public String authTest2(){
  return "您拥有'user:update'权限";
}

 

在客户端新增Spring Security配置类:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
}

 

改完后,先启动认证服务器,在启动客户端。

在启动客户端的时候出现异常:

Caused by: java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on cc.mrbird.sso.client.config.WebSecurityConfigurer$$EnhancerBySpringCGLIB$$aa470b71@34d45ec0, so it cannot be used on org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2SsoDefaultConfiguration$$EnhancerBySpringCGLIB$$6f69df92@18137eab too.
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.setFilterChainProxySecurityConfigurer(WebSecurityConfiguration.java:148) ~[spring-security-config-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_171]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_171]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_171]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_171]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:708) ~[spring-beans-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90) ~[spring-beans-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374) ~[spring-beans-5.1.8.RELEASE.jar:5.1.8.RELEASE]
... 16 common frames omitted

 

大致意思是,认证服务器已经配置了Spring Security配置,并且顺序为100,和客户端的Spring Security配置冲突了。所以我们修改下客户端的Spring Security配置顺序:

@Order(101)
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
}

 

让它的优先级小于认证服务器的Spring Security配置。

重新启动客户端,进行单点登录操作,登录成功后,访问http://127.0.0.1:9090/app1/auth/test1

访问http://127.0.0.1:9090/app1/auth/test2

返回403,没权限,说明权限注解生效了。

posted on 2019-10-06 12:13  houJINye  阅读(378)  评论(0编辑  收藏  举报