springboot2.0+oauth搭建SSO单点登录

新项目要开始了,所以想要使用spring boot来搭建架构,半天的时间,网上查阅各种资料,踩了诸多坑,成功实现单点登录,在此记录一下。

踩坑日记请查看springboot2.0+oauth搭建SSO单点登录之踩坑日记

本文只介绍环境搭建以及详细代码的编写,如果想要详细了解oauth2,请参阅阮一峰的理解OAuth 2.0

一、搭建springboot开发环境

使用最新的版本2.1.3.RELEASE进行搭建,pom文件


            org.springframework.boot
            spring-boot-starter-web
        

        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
            org.springframework.security.oauth
            spring-security-oauth2
            2.3.3.RELEASE
        

        
        
            org.apache.commons
            commons-lang3
            3.7
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            1.3.2
        

        
            mysql
            mysql-connector-java
            8.0.11
        

        
            com.alibaba
            druid
            RELEASE
        

        
            com.google.guava
            guava
            19.0
        

        
        
            com.github.pagehelper
            pagehelper-spring-boot-starter
            1.2.3
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        

环境搭建好之后,就要开始撸代码了(〜^㉨^)〜

二、主要代码

application.yml中将配置信息填写好

server:
  port: 9100
spring:
  application:
    name: ytsd-server
  mvc:
    throw-exception-if-no-handler-found: true
  datasource:
    url:  jdbc:mysql://localhost:3306/scoremanager?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
    username: admin
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
security:
  oauth2:
    resource:
      filter-order: 99

pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql
mybatis:
  type-aliases-package: com.ytsd.server.**.entity
  mapper-locations: classpath:com/ytsd/server/**/dao/*.xml


定义登录用的用户实体类User.java

package com.ytsd.server.test.entity;

import com.google.common.collect.Sets;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @Author: ysz
 * @Date: 2019-03-20 10:53
 */
@Data
public class User implements UserDetails {
    private String id;
    private String password;
    private String username;
    private String realName;
    private Set authorities = Sets.newHashSet();

    //权限列表
    @Override
    public Collection getAuthorities() {
        return this.authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
    }

    //获取密码
    @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;
    }
}

自定义数据库查询用户信息实体类MyUserDetailsService.java

package com.ytsd.server.test.service;

import com.ytsd.server.test.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

/**
 *
 * @Author: ysz
 * @Date: 2019-03-20 10:50
 */
@Component
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        User user=userService.findUserByName(userName);
        if(user==null){
            throw new UsernameNotFoundException("user not found");
        }
        return user;
    }
}

 配置认证/资源服务器

认证服务器AuthorizationServerConfiguration.java,在此配置clientId、secret、scope以及token过期时间。

package com.ytsd.server.configuration.oauth2;

import com.ytsd.server.configuration.oauth2.exception.OauthException;
import com.ytsd.server.test.entity.User;
import com.ytsd.server.test.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
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.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Author: ysz
 * @Date: 2019-03-20 11:23
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    @Autowired
    public MyUserDetailsService userDetailsService;


    @Autowired
    public OauthException oauthExceptionTranslator;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public TokenStore getTokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            if (accessToken instanceof DefaultOAuth2AccessToken) {
                DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
                Map additionalInformation = new LinkedHashMap<>();
                additionalInformation.put("expiration", accessToken.getExpiration().getTime());

                User user = (User) authentication.getPrincipal();
                additionalInformation.put("userName", user.getUsername());
                token.setAdditionalInformation(additionalInformation);
            }
            return accessToken;
        };
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .tokenEnhancer(tokenEnhancer())
                .exceptionTranslator(oauthExceptionTranslator)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(getTokenStore());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client_2")
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("all")
                .authorities("ROLE_APP")
                .secret(passwordEncoder().encode("123456"))
                .accessTokenValiditySeconds(60 * 30)
                .refreshTokenValiditySeconds(60 * 60);
    }

}

资源服务器ResourceServerConfiguration.java,它决定了哪些路径是需要认证的,比如我访问/captcha是不需要验证才可以访问到的,但是其他路径是需要认证后才可以访问。

package com.ytsd.server.configuration.oauth2;

import com.ytsd.server.configuration.security.CustomAccessDecisionManager;
import com.ytsd.server.configuration.security.CustomFilterInvocationSecurityMetadataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.access.intercept.FilterSecurityInterceptor;

/**
 * @Author: ysz
 * @Date: 2019-03-20 11:10
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    private static final String RESOURCE_ID = "*";


    @Autowired
    CustomAccessDecisionManager decisionManager;

    @Autowired
    CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID);
    }

    @Bean
    public FilterSecurityInterceptor filterSecurityInterceptor() {
        FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
        filterSecurityInterceptor.setAccessDecisionManager(decisionManager);
        filterSecurityInterceptor.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
        return filterSecurityInterceptor;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception{
        http
                .csrf().disable()
                .exceptionHandling()
                .and()
                .authorizeRequests()
                .antMatchers("/captcha/getCaptcha/*", "/captcha/checkCaptcha/*", "/druid/*")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .disable();
    }
}

自定义决策管理器

package com.ytsd.server.configuration.security;

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;

/**
 * @Author: ysz
 * @Date: 2019-03-20 10:23
 */
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object obj, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        HttpServletRequest request = ((FilterInvocation) obj).getHttpRequest();

        for (GrantedAuthority ga : authentication.getAuthorities()) {
            String authority = ga.getAuthority();
            String url = StringUtils.substringAfter(authority," ");
            String method = StringUtils.substringBefore(authority," ");

            AntPathRequestMatcher matcher = new AntPathRequestMatcher(url,method);
            if (matcher.matches(request)) {
                return;
            }
        }
        throw new AccessDeniedException("Access Denied");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class aClass) {
        return true;
    }
}

使用FilterInvocationSecurityMetadataSource来储存请求与权限的对应关系

一般会重写,实现FilterInvocationSecurityMetadataSource接口,FilterInvocationSecurityMetadataSource接口有3个方法:
boolean supports(Class clazz);
Collection getAllConfigAttributes();
Collection getAttributes(Object object);

 

第一个方法不清楚其作用, 一般返回true
第二个方法是Spring容器启动时自动调用的, 返回所有权限的集合. 一般把所有请求与权限的对应关系也要在这个方法里初始化, 保存在一个属性变量里
第三个是当接收到一个http请求时, filterSecurityInterceptor会调用的方法. 参数object是一个包含url信息的HttpServletRequest实例. 这个方法要返回请求该url所需要的所有权限集合

package com.ytsd.server.configuration.security;

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;

/**
 * @Author: ysz
 * @Date: 2019-03-20 10:53
 */
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Override
    public Collection getAllConfigAttributes() {
        Collection co=new ArrayList<>();
        co.add(new SecurityConfig("null"));
        return co;
    }

    @Override
    public Collection getAttributes(Object object) {
        return null;
    }

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

重写AuthenticationManager方法

网上查询到的很多资料都没有重写,但是在认证服务器中使用时报无法注入bean的异常,所有重写了一下

package com.ytsd.server.configuration.websecurity;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @Author: ysz
 * @Date: 2019-03-20 14:01
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

到这里基本就完成了,使用postman测试一下

springboot2.0+oauth搭建SSO单点登录_第1张图片

正确获取到token,单点登录成功

如有哪里写的有问题,或有侵权,请留言告知!

 因为有很多网友想要源码,所以特地上传了源码,请大家来这里或者在文章顶部自取

你可能感兴趣的:(Java,springboot)