摘要: 原创出处 http://www.iocoder.cn/Apollo/portal-auth-1/ 「芋道源码」欢迎转载,保留摘要,谢谢!

  • 1. 概述
  • 2. AuthConfiguration
  • 3. Users
  • 4. Authorities
  • 5. UserService
  • 6. UserInfoHolder
  • 7. SsoHeartbeatHandler
  • 8. LogoutHandler

1. 概述

老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》 ,特别是 《Portal 实现用户登录功能》 。

本文分享 Portal 的认证与授权,侧重在认证部分

在 《Portal 实现用户登录功能》 文档的开头:

Apollo 是配置管理系统,会提供权限管理(Authorization),理论上是不负责用户登录认证功能的实现(Authentication)。

所以 Apollo 定义了一些SPI用来解耦,Apollo 接入登录的关键就是实现这些 SPI 。

和我们理解的 JDK SPI 不同,Apollo 是基于 Spring Profile 的特性,配合上 Spring Java Configuration 实现了类似 SPI 的功能。对于大多数人,我们可能比较熟悉的是,基于不同的 Profile 加载不同环境yaml 或 properties 配置文件。所以,当笔者看到这样的玩法,也是眼前一亮。

在 apollo-portal 项目中,spi 包下,我们可以看到认证相关的配置实现,如下图所示:

  • 绿框:接口。
  • 紫框:实现。
  • 红框:配置接口对应的实现。
2. AuthConfiguration

com.ctrip.framework.apollo.portal.spi.configuration.AuthConfiguration ,认证 Spring Java 配置。如下图:

目前有三种实现:

  • 第一种, profile=ctrip ,携程内部实现,接入了SSO并实现用户搜索、查询接口。
  • 第二种,profile=auth ,使用 Apollo 提供的 Spring Security简单认证。
  • 第三种,profile 为空,使用默认实现,全局只有 apollo 一个账号。

一般情况下,我们使用第二种,基于 Spring Security 的实现。所以本文仅分享这种方式。对其他方式感兴趣的胖友,可以自己读下代码哈。

整体类图如下:

2.1 SpringSecurityAuthAutoConfiguration

UserService ,配置如下:

@Bean
@ConditionalOnMissingBean(UserService.class)
public UserService springSecurityUserService() {
    return new SpringSecurityUserService();
}
  • 使用 SpringSecurityUserService 实现类,在 「5. UserService」 中,详细解析。

UserInfoHolder ,配置如下:

@Bean
@ConditionalOnMissingBean(UserInfoHolder.class)
public UserInfoHolder springSecurityUserInfoHolder() {
    return new SpringSecurityUserInfoHolder();
}
  • 使用 SpringSecurityUserInfoHolder 实现类,在 「6. UserInfoHolder」 中,详细解析。

JdbcUserDetailsManager ,配置如下:

@Bean
public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilder auth, DataSource datasource) throws Exception {
    JdbcUserDetailsManager jdbcUserDetailsManager = auth.jdbcAuthentication() // 基于 JDBC
            .passwordEncoder(new BCryptPasswordEncoder()) // 加密方式为 BCryptPasswordEncoder
            .dataSource(datasource) // 数据源
            .usersByUsernameQuery("select Username,Password,Enabled from `Users` where Username = ?"// 使用 Username 查询 User
            .authoritiesByUsernameQuery("select Username,Authority from `Authorities` where Username = ?"// 使用 Username 查询 Authorities
            .getUserDetailsService();

    jdbcUserDetailsManager.setUserExistsSql("select Username from `Users` where Username = ?"); // 判断 User 是否存在
    jdbcUserDetailsManager.setCreateUserSql("insert into `Users` (Username, Password, Enabled) values (?,?,?)"); // 插入 User
    jdbcUserDetailsManager.setUpdateUserSql("update `Users` set Password = ?, Enabled = ? where Username = ?"); // 更新 User
    jdbcUserDetailsManager.setDeleteUserSql("delete from `Users` where Username = ?"); // 删除 User
    jdbcUserDetailsManager.setCreateAuthoritySql("insert into `Authorities` (Username, Authority) values (?,?)"); // 插入 Authorities
    jdbcUserDetailsManager.setDeleteUserAuthoritiesSql("delete from `Authorities` where Username = ?"); // 删除 Authorities
    jdbcUserDetailsManager.setChangePasswordSql("update `Users` set Password = ? where Username = ?"); // 更新 Authorities

    return jdbcUserDetailsManager;
}
  • org.springframework.security.provisioning.JdbcUserDetailsManager ,继承 JdbcDaoImpl 的功能,提供了一些很有用的与Users 和 Authorities 表相关的方法。
  • 胖友先看下 「3. Users」 和 「4. Authorities」 小节,然后回过头继续往下看。

SsoHeartbeatHandler ,配置如下:

@Bean
@ConditionalOnMissingBean(SsoHeartbeatHandler.class)
public SsoHeartbeatHandler defaultSsoHeartbeatHandler() {
    return new DefaultSsoHeartbeatHandler();
}
  • 使用 DefaultSsoHeartbeatHandler 实现类,在 「7. SsoHeartbeatHandler」 中,详细解析。

LogoutHandler ,配置如下:

@Bean
@ConditionalOnMissingBean(LogoutHandler.class)
public LogoutHandler logoutHandler() {
    return new DefaultLogoutHandler();
}
  • 使用 DefaultLogoutHandler 实现类,在 「8. LogoutHandler」 中,详细解析。

2.2 SpringSecurityConfigureration

@Order(99)
@Profile("auth")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
static class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter {

    public static final String USER_ROLE = "user";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); // 关闭打开的 csrf 保护
        http.headers().frameOptions().sameOrigin(); // 仅允许相同 origin 访问
        http.authorizeRequests()
                .antMatchers("/openapi/**""/vendor/**""/styles/**""/scripts/**""/views/**""/img/**").permitAll() // openapi 和 资源不校验权限
                .antMatchers("/**").hasAnyRole(USER_ROLE); // 其他,需要登录 User
        http.formLogin().loginPage("/signin").permitAll().failureUrl("/signin?#/error").and().httpBasic(); // 登录页
        http.logout().invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl("/signin?#/logout"); // 登出(退出)
        http.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/signin")); // 未身份校验,跳转到登录页
    }

}
  • @EnableWebSecurity 注解,禁用 Boot 的默认 Security 配置,配合 @Configuration 启用自定义配置(需要继承 WebSecurityConfigurerAdapter )。

  • @EnableGlobalMethodSecurity(prePostEnabled = true) 注解,启用 Security 注解,例如最常用的 @PreAuthorize 。

  • 注意.antMatchers("/**").hasAnyRole(USER_ROLE); 代码块,设置统一的 URL 的权限校验,只判断是否为登陆用户。另外,#hasAnyRole(...) 方法,会自动添加 "ROLE_" 前缀,所以此处的传参是 "user" 。代码如下:

    // ExpressionUrlAuthorizationConfigurer.java

    private static String hasAnyRole(String... authorities) {
     String anyAuthorities = StringUtils.arrayToDelimitedString(authorities,
       "','ROLE_");
     return "hasAnyRole('ROLE_" + anyAuthorities + "')";
    }
3. Users

Users 表,对应实体 com.ctrip.framework.apollo.portal.entity.po.UserPO ,代码如下:

@Entity
@Table(name = "Users")
public class UserPO {

    /**
     * 编号
     */

    @Id
    @GeneratedValue
    @Column(name = "Id")
    private long id;
    /**
     * 账号
     */

    @Column(name = "Username", nullable = false)
    private String username;
    /**
     * 密码
     */

    @Column(name = "Password", nullable = false)
    private String password;
    /**
     * 邮箱
     */

    @Column(name = "Email", nullable = false)
    private String email;
    /**
     * 是否开启
     */

    @Column(name = "Enabled", nullable = false)
    private int enabled;
    
}
  • 字段比较简单,胖友自己看注释。

3.1 UserInfo

com.ctrip.framework.apollo.portal.entity.bo.UserInfo ,UserBO 。代码如下:

public class UserInfo {

    /**
     * 账号 {@link com.ctrip.framework.apollo.portal.entity.po.UserPO#username}
     */

    private String userId;
    /**
     * 账号 {@link com.ctrip.framework.apollo.portal.entity.po.UserPO#username}
     */

    private String name;
    /**
     * 邮箱 {@link com.ctrip.framework.apollo.portal.entity.po.UserPO#email}
     */

    private String email;
    
}
  • 在 UserPO 的 #toUserInfo() 方法中,将 UserPO 转换成 UserBO ,代码如下:

    public UserInfo toUserInfo() {
        UserInfo userInfo = new UserInfo();
        userInfo.setName(this.getUsername());
        userInfo.setUserId(this.getUsername());
        userInfo.setEmail(this.getEmail());
        return userInfo;
    }
    • 注意userId 和 name 属性,都是指向 User.username 。
4. Authorities

Authorities 表,Spring Security 中的 Authority ,实际和 Role 角色等价。表结构如下:

`Id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id',
`Username` varchar(50NOT NULL,
`Authority` varchar(50NOT NULL,
  • 目前 Portal 只有一种角色 "ROLE_user" 。如下图所示:Apollo 源码解析 —— Portal 认证与授权(一)之认证_java
  • 为什么是这样的呢?在 Apollo 中,
    • 统一的 URL 的权限校验,只判断是否为登陆用户,在 SpringSecurityConfigureration 中,我们可以看到。
    • 具体每个 URL 的权限校验,通过在对应的方法上,添加 @PreAuthorize 方法注解,配合具体的方法参数,一起校验功能 + 数据级的权限校验。
5. UserService

com.ctrip.framework.apollo.portal.spi.UserService ,User 服务接口,用来给 Portal 提供用户搜索相关功能。代码如下:

public interface UserService {

    List<UserInfo> searchUsers(String keyword, int offset, int limit);

    UserInfo findByUserId(String userId);

    List<UserInfo> findByUserIds(List<String> userIds);

}

5.1 SpringSecurityUserService

com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService ,基于 Spring Security 的 UserService 实现类。

5.5.1 构造方法

private PasswordEncoder encoder = new BCryptPasswordEncoder();
/**
 * 默认角色数组,详细见 {@link #init()}
 */

private List<GrantedAuthority> authorities;

@Autowired
private JdbcUserDetailsManager userDetailsManager;
@Autowired
private UserRepository userRepository;

@PostConstruct
public void init() {
    authorities = new ArrayList<>();
    authorities.add(new SimpleGrantedAuthority("ROLE_user"));
}
  • authorities 属性,只有一个元素,为 "ROLE_user" 。

5.5.2 createOrUpdate

#createOrUpdate(UserPO) 方法,创建或更新 User 。代码如下:

  1@Transactional
  2public void createOrUpdate(UserPO user) {
  3:     String username = user.getUsername();
  4:     // 创建 Spring Security User
  5:     User userDetails = new User(username, encoder.encode(user.getPassword()), authorities);
  6:     // 若存在,则进行更新
  7:     if (userDetailsManager.userExists(username)) {
  8:         userDetailsManager.updateUser(userDetails);
  9:     // 若不存在,则进行新增
 10:     } else {
 11:         userDetailsManager.createUser(userDetails);
 12:     }
 13:     // 更新邮箱
 14:     UserPO managedUser = userRepository.findByUsername(username);
 15:     managedUser.setEmail(user.getEmail());
 16:     userRepository.save(managedUser);
 17: }
  • 第 5 行:创建 com.ctrip.framework.apollo.portal.spi.springsecurity.User 对象。
    • 使用  PasswordEncoder 对 password 加密。
    • 传入对应的角色 authorities 参数。
  • 第 6 至 12 行:新增或更新 User 。
  • 第 13 至 16 行:更新 email 。不直接在【第 6 至 12 行】处理的原因是,com.ctrip.framework.apollo.portal.spi.springsecurity.User 中没有 email 属性。

5.5.3 其他实现方法