微服务架构与应用SpringbootJava技术升华

spring cloud oauth2 搭建认证中心(授权服务

2018-08-16  本文已影响144人  pengyuyancode

单点登录

什么是单点登录?单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分

相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理。

oauth2

在oauth中我们通常将他分为认证中心和资源中心。二者可以放在一起,但是对于微服务来说,每个独自的微服务可能就是一个资源中心。
在oauth2中有几种授权模式:
1、授权码模式
2、密码模式
3、客户端模式
4、简易模式
5、刷新模式
具体请求流程不是本文中点,我们着重讲解Spring cloud oauth2搭建认证中心。

在oauth中我们通常将他分为认证中心和资源中心。
对于我们的微服务来说,每个独立的微服务即是一个个资源中心,用户想要请求微服务的数据,需要携带认证中心颁发的tocken。

1.引入相关pom依赖

这里我们使用1.5.2.RELEASE的spring boot版本,因为我们会将oauth2交给cloud管理,所以我们同时引入了cloud的依赖,cloud版本使用SR5

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.2.RELEASE</version>
    <relativePath></relativePath>
</parent>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Finchley.SR5</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
 <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!-- 热部署 -->
<!-- <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency> -->
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
        <optional>true</optional>
</dependency>
<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>

2.编写spring boot启动类

@SpringBootApplication
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = {
        "com.dahaonetwork.smartfactory.authserver.mapper"
        })
public class AuthenticationApplication {
    /** 主类  */
    public static void main(String[] args) {
        SpringApplication.run(AuthenticationApplication.class, args);
    }
}

3.声明一个授权服务器

声明一个授权服务器只需要继承 AuthorizationServerConfigurerAdapter,添加
@EnableAuthorizationServer 注解。
@EnableAuthorizationServer 这个注解告诉 Spring 这个应用是 OAuth2 的认证中心。
并且复写如下三个方法:
ClientDetailsServiceConfigurer:这个configurer定义了客户端细节服务。客户详细信息可以被初始化。
AuthorizationServerSecurityConfigurer:在令牌端点上定义了安全约束。
AuthorizationServerEndpointsConfigurer:定义了授权和令牌端点和令牌服务。

配置客户端详细步骤
ClientDetailsServiceConfigurer 类(AuthorizationServerConfigurer类中的一个调用类)可以用来定义一个基于内存的或者JDBC的客户端信息服务。
客户端对象重要的属性有:
clientId:(必须)客户端id。
secret:(对于可信任的客户端是必须的)客户端的私密信息。
scope:客户端的作用域。如果scope未定义或者为空(默认值),则客户端作用域不受限制。
authorizedGrantTypes:授权给客户端使用的权限类型。默认值为空。
authorities:授权给客户端的权限(Spring普通的安全权限)。
在运行的应用中,可以通过直接访问隐藏的存储文件(如:JdbcClientDetailsService中用到的数据库表)或者通过实现ClientDetailsManager 接口(也可以实现ClientDetailsService 接口,或者实现两个接口)来更新客户端信息。

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{...}
@Override 
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {...}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {...}

具体代码如下所示:这里我们将tocken信息存储在mysql中,分布式下你可以存储在redis中,全局共享。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisConnectionFactory connectionFactory;
    @Autowired
    private DataSource dataSource;
    /**
     * @Title: tokenStore
     * @Description: 用户验证信息的保存策略,可以存储在内存中,关系型数据库中,redis中
     * @param 
     * @return TokenStore
     * @throws
     */
    @Bean
    public TokenStore tokenStore(){
        //return new RedisTokenStore(connectionFactory);
        //return new InMemoryTokenStore();
        return new JdbcTokenStore(dataSource);
    }
    
    @Bean // 声明 ClientDetails实现
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }
    
    /**
     * 
     * 这个方法主要是用于校验注册的第三方客户端的信息,可以存储在数据库中,默认方式是存储在内存中,如下所示,注释掉的代码即为内存中存储的方式
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
        //       clients.inMemory()
        //       .withClient("client").secret("123456").scopes("read")
        //       .authorizedGrantTypes("authorization_code", "password", "refresh_token")
        //       .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT");
        //       //.authorizedGrantTypes("password","authorization_code","client_credentials","refresh_token");
        //       //.authorizedGrantTypes("password","refresh_token");
        //       //.redirectUris("https://www.getpostman.com/oauth2/callback");
        //   /*redirectUris 关于这个配置项,是在 OAuth2协议中,认证成功后的回调地址,此值同样可以配置多个*/
        
        
        clients.withClientDetails(clientDetails());
        clients.jdbc(dataSource);
    }
    
   
    /**
     * 这个方法主要的作用用于控制token的端点等信息
     */
    @Override 
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
        endpoints.tokenStore(tokenStore());
        //endpoints.userDetailsService(userService);
        // 配置TokenServices参数 可以考虑使用[DefaultTokenServices],它使用随机值创建令牌
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        tokenServices.setAccessTokenValiditySeconds( (int) TimeUnit.DAYS.toSeconds(30)); // 30天
        endpoints.tokenServices(tokenServices);
    }
    
    /**
     允许表单验证,浏览器直接发送post请求即可获取tocken
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
                "isAuthenticated()");
        oauthServer.allowFormAuthenticationForClients();
    }
}

4开启Spring Security的功能

spring security用来验证用户账号密码,对请求路劲做处理等。继承WebSecurityConfigurerAdapter 使用@EnableWebMvcSecurity 注解开启Spring Security的功能。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    
    @Autowired
    private MyAuthenticationProvider provider;
    
    @Autowired
    private UserDetailsService userService;
    /**
     * 用户认证
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(provider);
        auth.userDetailsService(userService);   
//      auth.inMemoryAuthentication()
//            .withUser("user").password("123456").authorities("ROLE_USER");
    }
    
    /**
     * 1:
     * 请求授权:
     * spring security 使用以下匹配器来匹配请求路劲:
     *      antMatchers:使用ant风格的路劲匹配
     *      regexMatchers:使用正则表达式匹配路劲
     * anyRequest:匹配所有请求路劲
     * 在匹配了请求路劲后,需要针对当前用户的信息对请求路劲进行安全处理。
     * 2:定制登录行为。
     *      formLogin()方法定制登录操作
     *      loginPage()方法定制登录页面访问地址
     *      defaultSuccessUrl()登录成功后转向的页面
     *      permitAll()
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
                http
                .authorizeRequests()
                .antMatchers(
                        StaticParams.PATHREGX.API, 
                        StaticParams.PATHREGX.CSS,
                        StaticParams.PATHREGX.JS,
                        StaticParams.PATHREGX.IMG).permitAll()//允许用户任意访问
                        .anyRequest().authenticated()//其余所有请求都需要认证后才可访问
                .and()
                .formLogin()
                    //.loginPage("/login/login.do")  /
                               //.defaultSuccessUrl("/hello2")
                        .permitAll();//允许用户任意访问
                http.csrf().disable();
    }
    
    /**
     * 密码模式下必须注入的bean authenticationManagerBean
     * 认证是由 AuthenticationManager 来管理的,
     * 但是真正进行认证的是 AuthenticationManager 中定义的AuthenticationProvider。
     *  AuthenticationManager 中可以定义有多个 AuthenticationProvider
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

5自定义用户认证实现

认证是由AuthenticationManager 来管理的,
但是真正进行认证的是 AuthenticationManager 中定义的AuthenticationProvider。
AuthenticationManager 中可以定义有多个 AuthenticationProvider,
我们在四步骤中AuthenticationManagerBuilder中添加了当前的Provider 。

@Named
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider  {


    /** 规则校验 */
    @Resource(name = "passwordService")
    private PasswordService passwordService;

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    // 构造函数中注入
    @Inject
    public MyAuthenticationProvider(UserDetailsService userDetailsService)
    {
        this.setUserDetailsService(userDetailsService);
    }

    /**
     * 自定义验证方式
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        
        MyUserDetails userDetails = (MyUserDetails) 
                this.getUserDetailsService().loadUserByUsername(username);

        //按登录规则校验用户
        passwordService.validateRules(userDetails.getUser(), password);

        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        Authentication authenticationToken = new UsernamePasswordAuthenticationToken(JSON.toJSONString(userDetails,SerializerFeature.WriteMapNullValue), password, authorities); 
        return authenticationToken;
        
    }

    @Override
    public boolean supports(Class<?> arg0) {
        return true;
    }

}

6自定义UserDetailsService

自定义需要实现UserDetailsService接口,并且重写loadUserByUsername方法。返回的用户信息需要实现UserDatails接口。

@Service("userInfo")
public class UserInfoService implements UserDetailsService{
    
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO Auto-generated method stub
        Map<String, String> paramMap=new HashMap<String, String>();
        paramMap.put("loginId", username);
        User user=userMapper.getUserByloginIds(paramMap);
        
        if(user ==null) {
            throw new BadCredentialsException(Constants.getReturnStr(Constants.USER_NOT_FOUND, Constants.USER_NOT_FOUND_TIPS));
        }
        MyUserDetails userDetails = new MyUserDetails();
        userDetails.setUserName(username);
        userDetails.setPassword(user.getPassword());
        userDetails.setUser(user);
        return userDetails;
    }

}
public class MyUserDetails implements UserDetails{
    
    private static final long seriaVersionUID=1L;
    
    private String userName;
    
    private String password;
    
    private User user;
    
    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setPassword(String password) {
        this.password = password;
    }
     
    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
    
    /**
     * 重写getAuthorities方法,将用户的角色作为权限
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //TODO 后续带完善
        return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_SUPER");
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

7相关数据库表结构

为了文章不过于冗余,表结构和代码将在将在gitlab上共享
https://gitlab.com/liguobao666/cloud-service

8相关配置文件

spring.application.name=oauth-server
server.port=8043
server.context-path=/uaa

logging.level.org.springframework.security=DEBUG
security.oauth2.resource.serviceId= ${PREFIX:}resource
security.oauth2.resource.filter-order=3

database.url=jdbc:mysql://192.168.7.175:3306/oauth2
spring.datasource.url=${database.url}?useUnicode\=true&characterEncoding\=UTF-8&useOldAliasMetadataBehavior\=true
spring.datasource.username=devdb
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver


logging.level.root=info
logging.level.com.dahaonetwork.smartfactory.authserver=debug
logging.level.org.springframework.security=info
#mybatis show sql
logging.level.org.springframework=WARN
spring.thymeleaf.cache=false
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

mybatis.mapper-locations=classpath:mapper/*.xml

9测试

这里我们使用密码模式来获取tocken:



这样认证服务器就搭建完成了,浏览器只需要携带tocken就可以访问资源服务器上的相关信息。

上一篇下一篇

猜你喜欢

热点阅读