目录

  • 环境准备
  • 注册中心
  • 认证中心和授权中心模块
  • 认证中心配置
  • 授权中心配置
  • 验证码配置
  • 资源服务器
  • 测试
  • 结尾



近排在搭一个Oauth+SpringSecurity的微服务框架,一来可以熟悉各个组件的使用,二来知道各组件如何搭配。

但是,这时候可能会有一些看官会说:“哎呀,网上不是很多这些已经搭建好了的框架吗?直接拿来就行啦,自己搭的话费时费力。”这就不对了啊,一名程序员要变强不仅仅要变秃,更要有钻研技术的能力哈。


环境准备

首先我这里发下目录结构:

sping security ldap集成操作_spring boot


bear-api模块:这里作为公共接口模块,主要给给他服务提供用户服务和日志服务。

bear-auth模块:认证中心和授权中心模块,用户在访问系统资源时需要在这里拿到授权后才能访问。

bear-common模块:公共模块,里面包括一些公共返回类、工具类、代码生成工具。

bear-gateway模块:网关模块,用的是Gateway作为网关中心。

bear-modules模块:资源模块,基本上系统的所有功能模块都在里面。

bear-server模块:注册中心,这里用的是Eureka。

注册中心

这没什么好说的,上一下我的配置文件:

server:
  port: 8090
eureka:
  instance:
    instance-id: bear-server
    hostname: 127.0.0.1
  client:
    fetch-registry: false
    register-with-eureka: false
    service-url:
      default-zone: http://${eureka.instance.hostname}:${server.port}/erueka/
 spring:
  application:
    name: bear-server
server:
    port: 8090
eureka:
  instance:
    instance-id: bear-server
    hostname: 127.0.0.1
  client:
    fetch-registry: false
    register-with-eureka: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/erueka/
  server:
      enable-self-preservation: true
      renewal-percent-threshold: 0.49

认证中心和授权中心模块

引入maven依赖:

<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.2</spring-cloud.version>
        <jdbc.version>2.4.5</jdbc.version>
        <kaptcha.version>2.3.2</kaptcha.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>pers.bear.framework.common.security</groupId>
            <artifactId>common-security</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>pers.bear.framework</groupId>
            <artifactId>bear-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>pers.bear.framework.common</groupId>
            <artifactId>common-core</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>${jdbc.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>${kaptcha.version}</version>
        </dependency>
    </dependencies>

里面有两个包common-core和common-security的依赖,我待会再说。上一些配置文件:

server:
  port: 8093
spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: bear-auth
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/bear_cloud?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
    hikari:
          maximum-pool-size: 100
  redis:
    host: 127.0.0.1
    port: 6379
eureka:
  instance:
    instance-id: bear-auth
    lease-expiration-duration-in-seconds: 30
    lease-renewal-interval-in-seconds: 20
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:8090/eureka

认证中心配置

首先我们来配置下认证中心:

/**
 * @author Mr.Bear
 * @date 2021/5/12 10:56
 *  认证服务器
 **/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;
    @Autowired
    private BearUserDetailService bearUserDetailService;
    @Autowired
    RedisConnectionFactory redisConnectionFactory;
    @Autowired
    DataSource dataSource;

	/**
     * 这里使用获取第三方客户端信息
     * @return
     */
    @Bean
    public ClientDetailsService clientDetailsService(){
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        // 重置查询sql语句
        jdbcClientDetailsService.setSelectClientDetailsSql("select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from sys_oauth_client_details where client_id = ?");
        return jdbcClientDetailsService;
    }

    /**
     * 校验注册的第三方客户端的信息,可以存储在数据库中,默认方式是存储在内存中,如下所示,注释掉的代码即为内存中存储的方式
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//        clients.inMemory()
//                .withClient("client")
//                .secret("1234").scopes("all")
//                .authorizedGrantTypes("authorization_code", "password", "refresh_token");
        clients.withClientDetails(clientDetailsService());
    }


    /**
     * 控制端点信息
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 使用默认DefaultTokenServices来创建令牌,令牌又随机值生成
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        // 设置过期时间
        int expire = (int) (SecretContants.EXPIRE / 1000);
        endpoints
                .authenticationManager(authenticationManager)
                .tokenStore(tokenStore())
                .userDetailsService(bearUserDetailService);
        defaultTokenServices.setTokenStore(endpoints.getTokenStore());
        defaultTokenServices.setAccessTokenValiditySeconds(expire);
        defaultTokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        endpoints.tokenServices(defaultTokenServices);
    }

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

因为我们是把第三方客户端存储在数据库中,所以需要spring-jdbc来进行获取,如果使用自定义表名的话就需要调用JdbcClientDetailsService中的setSelectClientDetailsSql来重写sql,下面给一下表设计和数据:

CREATE TABLE `sys_oauth_client_details`  (
  `client_id` varchar(48) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(11) NULL DEFAULT NULL,
  `refresh_token_validity` int(11) NULL DEFAULT NULL,
  `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `sys_oauth_client_details` VALUES ('client_system', NULL, '$2a$10$kmGjSqUNgpJWEfmKHPlN4ek3qIv8UepGkziJgQ/YruqRotUNhotVi', 'server', 'password', NULL, NULL, 3600, 7200, NULL, NULL);

SET FOREIGN_KEY_CHECKS = 1;

授权中心配置

接下来对SpringSecurity进行配置:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    BearUserDetailService bearUserDetailService;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 使用Bcrypt方式加密密码
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 以下请求皆忽略
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/js/**", "/css/**","/images/**");
    }

    /**
     * 添加验证码校验
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider(){
        AuthenticationProvider authenticationProvider = new CodeProvider();
        ((CodeProvider) authenticationProvider).setPasswordEncoder(passwordEncoder());
        ((CodeProvider) authenticationProvider).setUserDetailsService(bearUserDetailService);
        return authenticationProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(){
        ProviderManager providerManager = new ProviderManager(authenticationProvider());
        return providerManager;
    }



    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/oauth/**", "/code")
                .permitAll()
                .antMatchers(HttpMethod.POST, "/system/user", "/oper/log/insertLog")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable()
                .httpBasic()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
    }
}

UserDetailService主要是通过执行**loadUserByUsername()**方法获取登录用户对应的信息,包括权限、账号密码等等,最后把这些信息都封装在UserDetails对象中,并与用户输入的密码进行匹配。实际开发中,基本都是自定义UserDetailService,所以我也自己实现loadUserByUsername方法

@Service("bearUserDetailService")
public class BearUserDetailService implements UserDetailsService {
    @Autowired
    RemoteUserService remoteUserService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 获取用户信息,我这里用的feign远程调用,根据自己的实现方式来获取用户信息就行了
        RemoteUser remoteUser = remoteUserService.getUserInfoByUserName(s);

        if (Objects.isNull(remoteUser)) {
            throw new UsernameNotFoundException("用户不存在");
        }

        // 权限设置,把用户具有的权限标识和角色都放在 Set<SimpleGrantedAuthority>中
        List<RemoteRole> remoteRoleList = remoteUser.getPermissions();
        Set<SimpleGrantedAuthority> simpleGrantedAuthorities = new LinkedHashSet<>();
        if (Objects.nonNull(remoteRoleList) && remoteRoleList.size() > 0) {
            for (RemoteRole permission : remoteRoleList) {
                simpleGrantedAuthorities.add(new SimpleGrantedAuthority(permission.getRoleKey()));
            }
        }
        return new LoginUser(remoteUser.getUserId(), remoteUser.getUserName(), remoteUser.getPassword(), true, true, true, true, simpleGrantedAuthorities, remoteUser);
    }
}

可以看到我返回的是一个LoginUser对象,我这里扩展了UserDetails对象,往其中加入了更多的信息,上下代码先:

@Data
public class LoginUser implements UserDetails {
	// 用户ID
    private Long userId;
    // 用户名称
    private String username;
    // 用户密码
    private String password;
    // 用户对象信息
    private RemoteUser remoteUser;
    // 用户是否不过期
    private boolean isAccountNonExpired;
    // 用户是否不会被锁住
    private boolean isAccountNonLocked;
    // 密码是否不会过期
    private boolean isCredentialsNonExpired;
    // 这个我忘了
    private boolean isEnabled;
    // 是否管理员
    private boolean isAdmin;
    // 权限集合
    private Set<SimpleGrantedAuthority> authorities;

    public LoginUser(Long userId, String username, String password, boolean isAccountNonExpired, boolean isAccountNonLocked, boolean isCredentialsNonExpired, boolean isEnabled, Set<SimpleGrantedAuthority> authorities, RemoteUser remoteUser){
        this.authorities = authorities;
        this.username = username;
        this.remoteUser = remoteUser;
        this.userId = userId;
        this.password = password;
        this.isAccountNonExpired = isAccountNonExpired;
        this.isAccountNonLocked = isAccountNonLocked;
        this.isCredentialsNonExpired = isCredentialsNonExpired;
        this.isEnabled = isEnabled;
        this.isAdmin = false;
    }

    public LoginUser(Long userId, String username, String password, boolean isAccountNonExpired, boolean isAccountNonLocked, boolean isCredentialsNonExpired, boolean isEnabled, Set<SimpleGrantedAuthority> authorities, RemoteUser remoteUser, boolean isAdmin){
        this.authorities = authorities;
        this.username = username;
        this.remoteUser = remoteUser;
        this.userId = userId;
        this.password = password;
        this.isAccountNonExpired = isAccountNonExpired;
        this.isAccountNonLocked = isAccountNonLocked;
        this.isCredentialsNonExpired = isCredentialsNonExpired;
        this.isEnabled = isEnabled;
        this.isAdmin = isAdmin;
    }

    public LoginUser(String username, String password, Set<SimpleGrantedAuthority> authorities){
        this.authorities = authorities;
        this.username = username;
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

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

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

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

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

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

其中用户信息和是否管理员是我自己加上去的,各位看官可以根据实际情况加上自己需要的信息。

验证码配置

授权和认证都配置好了,为了安全起见,我们需要加到个验证码校验,实现方式如下:

/**
 * @author Mr.Bear
 * @date 2021/5/23 13:40
 * 验证码配置
 **/
@Configuration
public class KaptchaConfig {

    @Bean
    Producer kaptcha(){
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "150");
        properties.setProperty("kaptcha.image.height", "50");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        properties.setProperty("kaptcha.textproducer.char.length", "5");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

验证码生成接口:

@RestController
public class CodeController {

    @Autowired
    Producer producer;

    @GetMapping("/code")
    public void getVerifyCode(HttpServletResponse response, HttpSession session) throws IOException {
        response.setContentType("image/jpeg");
        String text = producer.createText();
        session.setAttribute("code", text);
        BufferedImage image = producer.createImage(text);
        try(ServletOutputStream outputStream = response.getOutputStream()){
            ImageIO.write(image, "jpg", outputStream);
        }
    }
}

接下来需要新建一个provider将验证码的校验加入到springsercurity的校验流程中:

public class CodeProvider extends DaoAuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
        String verifyCode = request.getParameter("code");
        String sessionCode = (String) request.getSession().getAttribute("code");
        if(Objects.nonNull(verifyCode) && Objects.nonNull(sessionCode) && sessionCode.toLowerCase().equals(verifyCode.toLowerCase())){
            return super.authenticate(authentication);
        }
        throw new AuthenticationServiceException("验证码输入错误");
    }
}

最后上下Application的代码:

@SpringBootApplication
// 不用feign的话可以不用加
@EnableFeignClients(basePackages = {"pers.bear"})
@EnableEurekaClient
public class BearAuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(BearAuthApplication.class, args);
    }

}

资源服务器

认证和授权服务器搭建好了,接下来我们配置下资源服务,因为有时候资源服务器可能很多,如果一个个配置的话就很麻烦了,我这里就把这个资源配置放到common-security模块中,资源需要保护的话直接引入该模块就行了。

sping security ldap集成操作_ide_02


这是ResourceOauthConfig的代码:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法权限注解
public class ResourceOauthConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    RedisConnectionFactory redisConnectionFactory;

	/**
     * 使用redis存储token
     * @return
     */
    @Bean
    public TokenStore tokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/system/user/info")
                .permitAll()
                .antMatchers(HttpMethod.POST, "/system/user", "/oper/log/insertLog")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }

}

SecurityUtils工具类能够获取当前用户的信息。

public class SecurityUtils {

    public static Authentication getAuthenticaion(){
        return SecurityContextHolder.getContext().getAuthentication();
    }

    public static LoginUser getLoginUser(){
        return (LoginUser) getAuthenticaion().getPrincipal();
    }

    public static RemoteUser getRemoteUser(){
        return getLoginUser().getRemoteUser();
    }

    public static Long getUserId(){
        return getLoginUser().getUserId();
    }
}

因为依赖引入的时候,Spring只会扫描到本目录的注解,其它模块的注解是扫描不到的,因此需要在spring.factories中加入以下一段话:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    pers.bear.framework.common.security.config.ResourceOauthConfig

接下来我们需要用bear-system模块来进行实验:

sping security ldap集成操作_java_03


配置文件跟认证中心的配置文件差不多,就改下instance-id这些地方,我这里就不上了。上下测试controller:

@RestController
@RequestMapping("/system/user")
public class UserController {

    @Autowired
    IUserService userService;

    @GetMapping("list")
    public Response list(){
        List<User> userList = userService.list();
        return Response.success(userList);
    }

application的代码

@SpringBootApplication
@EnableEurekaClient
// 开启资源服务器
@EnableResourceServer
public class BearSystemApplication {

    public static void main(String[] args) {
        SpringApplication.run(BearSystemApplication.class, args);
    }

}

到这里基本上完成搭建。

测试

到了最激动人心的测试环节了。我用postman来测试。首先获取一下验证码:

sping security ldap集成操作_spring_04


然后请求/oauth/token来获取访问token

sping security ldap集成操作_bc_05


access_token就是我们想要的,接下来携带这个token去请求我们想访问的接口:

sping security ldap集成操作_ide_06


访问成功,如果不带token的话就会出现401错误:

sping security ldap集成操作_spring_07

结尾

到这里整个流程基本完成了,各位有什么疑问的话可以在评论区反映,毕竟我写的也不是最好的解决方案哈哈。