# springSecurity

back:Spring | 返回:权限

# 默认情况

back

//源码
@Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            if (auth.isConfigured()) {
                return;
            }
            User user = this.securityProperties.getUser();
            if (user.isDefaultPassword()) {
                logger.info(String.format("%n%nUsing default security password: %s%n",
                        user.getPassword()));
            }
            Set<String> roles = new LinkedHashSet<>(user.getRole());
            withUser(user.getName()).password(user.getPassword())
                    .roles(roles.toArray(new String[roles.size()]));
            setField(auth, "defaultUserDetailsService", getUserDetailsService());
            super.configure(auth);
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

是SecurityProperties这个Bean管理了用户名和密码。
在SecurityProperties里面的一个内部静态类User类里面,管理了默认的认证的用户名与密码。

security默认的用户名是user, 默认密码是应用启动的时候,通过UUID算法随机生成的。默认的role是"USER"。

//源码
public static class User {

        /**
         * Default user name.
         */
        private String name = "user";

        /**
         * Password for the default user name.
         */
        private String password = UUID.randomUUID().toString();

        /**
         * Granted roles for the default user name.
         */
        private List<String> role = new ArrayList<>(Collections.singletonList("USER"));

        private boolean defaultPassword = true;

        public String getName() {
            return this.name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getPassword() {
            return this.password;
        }

        public void setPassword(String password) {
            if (password.startsWith("${") && password.endsWith("}")
                    || !StringUtils.hasLength(password)) {
                return;
            }
            this.defaultPassword = false;
            this.password = password;
        }

        public List<String> getRole() {
            return this.role;
        }

        public void setRole(List<String> role) {
            this.role = new ArrayList<>(role);
        }

        public boolean isDefaultPassword() {
            return this.defaultPassword;
        }

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

当然也可以在配置文件里修改

spring.security.user.name=user
spring.security.user.password=123456
1
2

当然这只是一个初级的配置,更复杂的配置,可以分不同角色,在控制范围上,能够拦截到方法级别的权限控制

# 中阶Security,内存用户名密码认证

back

我们来定制用户名密码。

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true
        ,securedEnabled = true
        ,jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
//        http.authorizeRequests().anyRequest().permitAll().and().csrf().disable();
        //csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("htring")
                .password("123456")
                .roles("USER");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

简要说明:

  1. 通过 @EnableWebSecurity注解开启Spring Security的功能。使用@EnableGlobalMethodSecurity(prePostEnabled = true)这个注解,可以开启security的注解,我们可以在需要控制权限的方法上面使用@PreAuthorize,@PreFilter这些注解。
  2. extends 继承 WebSecurityConfigurerAdapter 类,并重写它的方法来设置一些web安全的细节。我们结合@EnableWebSecurity注解和继承WebSecurityConfigurerAdapter,来给我们的系统加上基于web的安全机制。
  3. 在configure(HttpSecurity http)方法里面,默认的认证代码是:
protected void configure(HttpSecurity http) throws Exception {
    logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin().and()
        .httpBasic();
}
1
2
3
4
5
6
7
8
9
10

登陆时发生错误,There is no PasswordEncoder mapped for the id "null"

关于 Spring Security 5.0.X 的说明:
在Spring Security 5.0之前,PasswordEncoder 的默认值为 NoOpPasswordEncoder 既表示为纯文本密码,在实际的开发过程中 PasswordEncoder 大多数都会设值为 BCryptPasswordEncoder ,但是这样会导致几个问题:
1、在应用程序中使用 BCryptPasswordEncoder 编码方式编码后的密码,很难轻松的迁移;
2、密码存储后,会再次被更改;
3、作为一个应用中的安全框架,Spring Security 不能频繁地进行中断更改;
在 Spring Security 5.0.x 以后,密码的一般格式为:{ID} encodedPassword ,ID 主要用于查找 PasswordEncoder 对应的编码标识符,并且encodedPassword 是所选的原始编码密码 PasswordEncoder。ID 必须书写在密码的前面,开始用{,和 结束 }。如果 ID 找不到,ID 则为null。例如,在相关的源码中,我找到了 Spring Security 定义的不同的编码方式的列表 ID。所有原始密码都是“ password ”。

解决方案:

1.Add password storage format, for plain text, add {noop}

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("htring")
                .password("{noop}123456")
                .roles("USER","ADMIN")
                .and()
                .withUser("clm")
                .password("{noop}123456")
                .roles("USER");
    }
1
2
3
4
5
6
7
8
9
10
11

2.User.withDefaultPasswordEncoder() for UserDetailsService

已经废弃的方法

 User.UserBuilder users = User.withDefaultPasswordEncoder();
1

3.新建一个 PasswordEncoder 并实现 PasswordEncoder 接口

public class CustomPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

在前面修改的用户名密码最后加上

.and().passwordEncoder(new CustomPasswordEncoder())
1

4.我们要将前端传过来的密码进行某种方式加密,spring security 官方推荐的是使用bcrypt加密方式。

auth.inMemoryAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("htring")
                .password(new BCryptPasswordEncoder().encode("123456"))
                .roles("USER","ADMIN")
                .and()
                .withUser("clm")
                .password(new BCryptPasswordEncoder().encode("123456"))
                .roles("USER");
1
2
3
4
5
6
7
8
9

# 角色权限控制

当我们的系统功能模块当需求发展到一定程度时,会不同的用户,不同角色使用我们的系统。这样就要求我们的系统可以做到,能够对不同的系统功能模块,开放给对应的拥有其访问权限的用户使用。
Spring Security提供了Spring EL表达式,允许我们在定义URL路径访问(@RequestMapping)的方法上面添加注解,来控制访问权限。
在标注访问权限时,根据对应的表达式返回结果,控制访问权限:

  • true,表示有权限
  • fasle,表示无权限

Spring Security可用表达式对象的基类是SecurityExpressionRoot

通过阅读源码,我们可以更加深刻的理解其EL写法,并在写代码的时候正确的使用。变量defaultRolePrefix硬编码约定了role的前缀是"ROLE_"

同时,我们可以看出hasRolehasAnyRole是一样的。hasAnyRole是调用的hasAnyAuthorityName(defaultRolePrefix, roles)。所以,我们在学习一个框架或者一门技术的时候,最准确的就是源码。通过源码,我们可以更好更深入的理解技术的本质。

表达式 描述
hasRole([role]) 当前用户是否拥有指定角色。
hasAnyRole([role1,role2]) 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。
hasAuthority([auth]) 等同于hasRole
hasAnyAuthority([auth1,auth2]) 等同于hasAnyRole
Principle 代表当前用户的principle对象
authentication 直接从SecurityContext获取的当前Authentication对象
permitAll 总是返回true,表示允许所有的
denyAll 总是返回false,表示拒绝所有的
isAnonymous() 当前用户是否是一个匿名用户
isRememberMe() 表示当前用户是否是通过Remember-Me自动登录的
isAuthenticated() 表示当前用户是否已经登录认证成功了。
isFullyAuthenticated() 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。
@GetMapping("/getHello")
    @PreAuthorize("hasRole('ADMIN')")
    @ApiOperation(value = "获取session中的值",notes = "获取session中的值,确保共享成功")
    public String gHello(HttpSession httpSession){
        return Optional.ofNullable(httpSession.getAttribute("name"))
                .orElse("星矢")+"你好,来自"+port;
    }
1
2
3
4
5
6
7

在方法上添加@PreAuthorize这个注解,value="hasRole('ADMIN')")是Spring-EL expression,当表达式值为true,标识这个方法可以被调用。如果表达式值是false,标识此方法无权限访问。

# 在Spring-security里面获取当前登录认证通过的用户信息

我们添加一个LoginFilter,默认拦截所有请求,把当前登录的用户放到系统session中即可。在Spring Security中,用户信息保存在SecurityContextHolder中。Spring Security使用一个Authentication对象来持有所有系统的安全认证相关的信息。这个信息的内容格式如下:

{
    "accountNonExpired":true,
    "accountNonLocked":true,
    "authorities":[{
        "authority":"ROLE_ADMIN"
    },{
        "authority":"ROLE_USER"
    }],
    "credentialsNonExpired":true,
    "enabled":true,
    "username":"root"
}
1
2
3
4
5
6
7
8
9
10
11
12

这个Authentication对象信息其实就是User实体的信息(当然,密码没放进来)。

public class User implements UserDetails, CredentialsContainer {
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;
        ....
}
1
2
3
4
5
6
7
8
9
10

通过调用getContext()返回的对象是SecurityContext的实例对象,该实例对象保存在ThreadLocal线程本地存储中。使用Spring Security框架,通常的认证机制都是返回UserDetails实例。

Spring MVC的 Web开发使用 Controller 基本上可以完成大部分需求,但是我们还可能会用到 Servlet、Filter、Listener、Interceptor 等等。

在Spring Boot中添加自己的Servlet有两种方法,代码注册Servlet和注解自动注册(Filter和Listener也是如此)。

(1)代码注册通过ServletRegistrationBean、 FilterRegistrationBean 和 ServletListenerRegistrationBean 获得控制。 也可以通过实现 ServletContextInitializer 接口直接注册。使用代码注册Servlet(就不需要@ServletComponentScan注解)
(2)在 SpringBootApplication 上使用@ServletComponentScan 注解后,Servlet、Filter、Listener 可以直接通过 @WebServlet、@WebFilter、@WebListener 注解自动注册。

下面我们就采用第(2)种方法,通过添加一个LoginFilter,拦截所有请求,把当前登录信息放到系统session中,并在前端页面显示。

1.添加一个实现了javax.servlet.Filter的LoginFilter,把当前登录信息放到系统session中

@Order(1)
@WebFilter(filterName = "loginFilter",urlPatterns = {"/*"})
@Slf4j
public class LoginFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpSession httpSession = ((HttpServletRequest)request).getSession(true);
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        log.info("user info: [{}]", JSON.toJSONString(user, SerializerFeature.PrettyFormat));
        String userName;
            userName = user.getUsername();
        httpSession.setAttribute("userName",userName);
        chain.doFilter(request,response);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

2.给启动类加上注解@ServletComponentScan

@SpringBootApplication
@EnableScheduling
@ServletComponentScan("com.chlm.mysession.filter")
public class MysessionApplication {
    public static void main(String[] args) {
        SpringApplication.run(MysessionApplication.class, args);
    }
}
1
2
3
4
5
6
7
8

其中,@WebFilter(filterName = "loginFilter", urlPatterns = Array("/*")) ,这个注解用来声明一个Servlet的Filter,这个加注解@WebFilter的LoginFilter类必须要实现javax.servlet.Filter接口。它会在容器部署的时候扫描处理。如果不指定urlPatterns,默认url-pattern是/。这个@WebFilter注解,在SpringBoot中,要给启动类加上注解@ServletComponentScan,开启扫描Servlet组件功能。

这个注解将开启扫描Servlet组件功能。那些被标注了@WebFilter,@WebServlet,@WebListener的Bean将会注册到容器中。需要注意的一点是,这个扫描动作只在当我们使用的是嵌入式Servlet容器的时候才起作用。完成Bean注册工作的类是org.springframework.boot.web.servlet.ServletComponentScanRegistrar,它实现了Spring的ImportBeanDefinitionRegistrar接口。

3.前端显示用户信息

Velocity内置了一些对象,例如:$request、$response、$session,这些对象可以在vm模版里可以直接调用。所以我们只需要使用$session取出,当初我们放进session的对应key的属性值即可。

<div class="pull-left info">
    <p>$session.getAttribute('userName')</p>
    <a href="#"><i class="fa fa-circle text-success"></i> Online</a>
</div>
1
2
3
4

# 进阶Security:用数据库存储用户和角色,实现安全认证

back

本节我们将在我们之前的系统上,实现一个用数据库存储用户和角色,实现系统的安全认证。在权限角色上,我们简单设计两个用户角色:USER,ADMIN。

# 架构框架-Component、Service、Filter分析

back

想要深入spring security的authentication (身份验证)和access-control(访问权限控制)工作流程,必须清楚spring security的主要技术点包括关键接口、类以及抽象类如何协同工作进行authentication 和access-control的实现。

# spring-security认证和授权流程

常见认证和授权流程可以分成:

  1. A user is prompted to log in with a username and password (用户用账密码登录)
  2. The system (successfully) verifies that the password is correct for the username(校验密码正确性)
  3. The context information for that user is obtained (their list of roles and so on).(获取用户信息context,如权限)
  4. A security context is established for the user(为用户创建security context)
  5. The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.(访问权限控制,是否具有访问权限)

# 1.1 spring security 认证

上述前三点为spring security认证验证环节:

  1. 通常通过AbstractAuthenticationProcessingFilter过滤器将账号密码组装成Authentication实现类UsernamePasswordAuthenticationToken;
  2. 将token传递给AuthenticationManager验证是否有效,而AuthenticationManager通常使用ProviderManager实现类来检验;
  3. AuthenticationManager认证成功后将返回一个拥有详细信息的Authentication object(包括权限信息,身份信息,细节信息,但密码通常会被移除);
  4. 通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()将Authentication设置到security context中。

# 1.2 spring security访问授权

  1. 通过FilterSecurityInterceptor过滤器入口进入;
  2. FilterSecurityInterceptor通过其继承的抽象类的AbstractSecurityInterceptor.beforeInvocation(Object object)方法进行访问授权,其中涉及了类AuthenticationManager、AccessDecisionManager、SecurityMetadataSource等。

根据上述描述的过程,我们接下来主要去分析其中涉及的一下Component、Service、Filter。

# 核心组件(Core-Component)

back_home

back

# 2.1 SecurityContextHolder

SecurityContextHolder提供对SecurityContext的访问,存储security context(用户信息、角色权限等),而且其具有下列储存策略即工作模式:

  1. SecurityContextHolder.MODE_THREADLOCAL(默认):使用ThreadLocal,信息可供此线程下的所有的方法使用,一种与线程绑定的策略,此天然很适合Servlet Web应用。
  2. SecurityContextHolder.MODE_GLOBAL:使用于独立应用
  3. SecurityContextHolder.MODE_INHERITABLETHREADLOCAL:具有相同安全标示的线程

修改SecurityContextHolder的工作模式有两种方法 :

  1. 设置一个系统属性(system.properties) : spring.security.strategy;
  2. 调用SecurityContextHolder静态方法setStrategyName()

在默认ThreadLocal策略中,SecurityContextHolder为静态方法获取用户信息为:

//源码
  Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
   if (principal instanceof UserDetails) {
        String username = ((UserDetails)principal).getUsername();
   } else {
        String username = principal.toString();
   }
1
2
3
4
5
6
7

但是一般不需要自身去获取。其中getAuthentication()返回一个Authentication认证主体,接下来分析Authentication、UserDetails细节。

# 2.2 Authentication

Spring Security使用一个Authentication对象来描述当前用户的相关信息,其包含用户拥有的权限信息列表、用户细节信息(身份信息、认证信息)。Authentication为认证主体在spring security中时最高级别身份/认证的抽象,常见的实现类UsernamePasswordAuthenticationToken。Authentication接口源码:

//源码
public interface Authentication extends Principal, Serializable {
    //权限信息列表,默认GrantedAuthority接口的一些实现类
    Collection<? extends GrantedAuthority> getAuthorities();
    //密码信息
    Object getCredentials();
    //细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值
    Object getDetails();
    //通常返回值为UserDetails实现类
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

前面两个组件都涉及了UserDetails,以及GrantedAuthority其到底是什么呢?2.3小节分析。

# 2.3 UserDetails&GrantedAuthority

UserDetails提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息,包含GrantedAuthority。其官方实现类为User,开发者可以实现其接口自定义UserDetails实现类。其接口源码:

//源码
 public interface UserDetails extends Serializable {

     Collection<? extends GrantedAuthority> getAuthorities();

     String getPassword();

     String getUsername();

     boolean isAccountNonExpired();

     boolean isAccountNonLocked();

     boolean isCredentialsNonExpired();

     boolean isEnabled();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

UserDetailsAuthentication接口功能类似,其实含义即是Authentication为用户提交的认证凭证(账号密码)UserDetails为系统中用户正确认证凭证,在UserDetailsService中的loadUserByUsername方法获取正确的认证凭证。
其中在getAuthorities()方法中获取到GrantedAuthority列表是代表用户访问应用程序权限范围,此类权限通常是“role(角色)”,例如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。GrantedAuthority接口常见的实现类SimpleGrantedAuthority。

# 核心服务类(Core-Services)

back

# 3.1 AuthenticationManager、ProviderManager以及AuthenticationProvider

AuthenticationManager是认证相关的核心接口,是认证一切的起点。但常见的认证流程都是AuthenticationManager实现类ProviderManager处理,而且ProviderManager实现类基于委托者模式维护AuthenticationProvider 列表用于不同的认证方式。例如:

  1. 使用账号密码认证方式DaoAuthenticationProvider实现类(继承了AbstractUserDetailsAuthenticationProvide抽象类),其为默认认证方式,进行数据库库获取认证数据信息。
  2. 游客身份登录认证方式AnonymousAuthenticationProvider实现类
  3. 从cookies获取认证方式RememberMeAuthenticationProvider实现类

AuthenticationProvider为

ProviderManager源码分析:

//源码
public Authentication authenticate(Authentication authentication)
    throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
//AuthenticationProvider列表依次认证
for (AuthenticationProvider provider : getProviders()) {
    if (!provider.supports(toTest)) {
        continue;
    }
    try {
        //每个AuthenticationProvider进行认证
        result = provider.authenticate(authentication)
        if (result != null) {
            copyDetails(authentication, result);
            break;
        }
    }
    ....
    catch (AuthenticationException e) {
        lastException = e;
    }
}
//进行父类AuthenticationProvider进行认证
if (result == null && parent != null) {
    // Allow the parent to try.
    try {
        result = parent.authenticate(authentication);
    }
    catch (AuthenticationException e) {
        lastException = e;
    }
}
    // 如果有Authentication信息,则直接返回
if (result != null) {
    if (eraseCredentialsAfterAuthentication
            && (result instanceof CredentialsContainer)) {
            //清除密码
        ((CredentialsContainer) result).eraseCredentials();
    }
    //发布登录成功事件
    eventPublisher.publishAuthenticationSuccess(result);
    return result;
}
    //如果都没认证成功,抛出异常
if (lastException == null) {
    lastException = new ProviderNotFoundException(messages.getMessage(
            "ProviderManager.providerNotFound",
            new Object[] { toTest.getName() },
            "No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
    }  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

ProviderManager 中的AuthenticationProvider列表,会依照次序去认证,默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功,而且AuthenticationProvider认证成功后返回一个Authentication实体,并为了安全会进行清除密码。如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。

# 3.2 UserDetailsService

UserDetailsService接口作用是从特定的地方获取认证的数据源(账号、密码)。如何获取到系统中正确的认证凭证,通过loadUserByUsername(String username)获取认证信息,而且其只有一个方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;  
1

其常见的实现类从数据获取的JdbcDaoImpl实现类,从内存中获取的InMemoryUserDetailsManager实现类,不过我们可以实现其接口自定义UserDetailsService实现类,如下:

//源码
public class CustomUserService implements UserDetailsService {
 @Autowired
 //用户mapper
 private UserInfoMapper userInfoMapper;
 @Autowired
 //用户权限mapper
 private PermissionInfoMapper permissionInfoMapper;
 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserInfoDTO userInfo = userInfoMapper.getUserInfoByUserName(username);
    if (userInfo != null) {
        List<PermissionInfoDTO> permissionInfoDTOS = permissionInfoMapper.findByAdminUserId(userInfo.getId());
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        //组装权限GrantedAuthority object
        for (PermissionInfoDTO permissionInfoDTO : permissionInfoDTOS) {
            if (permissionInfoDTO != null && permissionInfoDTO.getPermissionName() != null) {
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(
                        permissionInfoDTO.getPermissionName());
                grantedAuthorityList.add(grantedAuthority);
            }
        }
        //返回用户信息
        return new User(userInfo.getUserName(), userInfo.getPasswaord(), grantedAuthorityList);
    }else {
        //抛出用户不存在异常
        throw new UsernameNotFoundException("admin" + username + "do not exist");
      }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 3.3 AccessDecisionManager&SecurityMetadataSource

AccessDecisionManager是由AbstractSecurityInterceptor调用,负责做出最终的访问控制决策。 AccessDecisionManager接口源码:

 //访问控制决策
  void decide(Authentication authentication, Object secureObject,Collection<ConfigAttribute> attrs)
        throws AccessDeniedException;
  //是否支持处理传递的ConfigAttribute
  boolean supports(ConfigAttribute attribute);
  //确认class是否为AccessDecisionManager
  boolean supports(Class clazz);
1
2
3
4
5
6
7

SecurityMetadataSource包含着AbstractSecurityInterceptor访问授权所需的元数据(动态url、动态授权所需的数据),在AbstractSecurityInterceptor授权模块中结合AccessDecisionManager进行访问授权。其涉及了ConfigAttribute。

SecurityMetadataSource接口:

Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException;

Collection<ConfigAttribute> getAllConfigAttributes();

boolean supports(Class<?> clazz);
1
2
3
4
5
6

我们还可以自定义SecurityMetadataSource数据源,实现接口FilterInvocationSecurityMetadataSource。例:

public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    public List<ConfigAttribute> getAttributes(Object object) {
        FilterInvocation fi = (FilterInvocation) object;
        String url = fi.getRequestUrl();
        String httpMethod = fi.getRequest().getMethod();
        List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();

        // Lookup your database (or other source) using this information and populate the
        // list of attributes

        return attributes;
    }

    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 3.4 PasswordEncoder

为了存储安全,一般要对密码进行算法加密,而spring security提供了加密PasswordEncoder接口。其实现类有使用BCrypt hash算法实现的BCryptPasswordEncoder,SCrypt hashing 算法实现的SCryptPasswordEncoder实现类,实现类内部实现可看源码分析。而PasswordEncoder接口只有两个方法:

public interface PasswordEncoder {
    //密码加密
    String encode(CharSequence rawPassword);
    //密码配对
    boolean matches(CharSequence rawPassword, String encodedPassword);
}
1
2
3
4
5
6

# 核心Security过滤器(Core-Security-Filters)

back

# 4.1 FilterSecurityInterceptor

FilterSecurityInterceptor是Spring security授权模块入口,该类根据访问的用户的角色,权限授权访问那些资源(访问特定路径应该具备的权限)。
FilterSecurityInterceptor封装FilterInvocation对象进行操作,所有的请求到了这一个filter,如果这个filter之前没有执行过的话,那么首先执行其父类AbstractSecurityInterceptor提供的InterceptorStatusToken token = super.beforeInvocation(fi),在此方法中使用AuthenticationManager获取Authentication中用户详情,使用ConfigAttribute封装已定义好访问权限详情,并使用AccessDecisionManager.decide()方法进行访问权限控制。

FilterSecurityInterceptor源码分析:

//源码
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
        && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
        && observeOncePerRequest) {
    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
    // first time this request being called, so perform security checking
    if (fi.getRequest() != null && observeOncePerRequest) {
        fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
    }
    //回调其继承的抽象类AbstractSecurityInterceptor的方法
    InterceptorStatusToken token = super.beforeInvocation(fi);

    try {
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    }
    finally {
        super.finallyInvocation(token);
    }

    super.afterInvocation(token, null);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

AbstractSecurityInterceptor源码分析:

protected InterceptorStatusToken beforeInvocation(Object object) {
....
//获取所有访问权限(url-role)属性列表(已定义在数据库或者其他地方)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
        .getAttributes(object);
....
//获取该用户访问信息(包括url,访问权限)
Authentication authenticated = authenticateIfRequired();

// Attempt authorization
try {
    //进行授权访问
    this.accessDecisionManager.decide(authenticated, object, attributes);
}catch
....
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4.2 UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter使用username和password表单登录使用的过滤器,也是最为常用的过滤器。其源码:

public Authentication attemptAuthentication(HttpServletRequest request,
    HttpServletResponse response) throws AuthenticationException {
     //获取表单中的用户名和密码
     String username = obtainUsername(request);
     String password = obtainPassword(request);
     ...
     username = username.trim();
     //组装成username+password形式的token
     UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
     username, password);
     // Allow subclasses to set the "details" property
     setDetails(request, authRequest);
     //交给内部的AuthenticationManager去认证,并返回认证信息
     return this.getAuthenticationManager().authenticate(authRequest);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

其主要代码为创建UsernamePasswordAuthenticationTokenAuthentication实体以及调用AuthenticationManager进行authenticate认证,根据认证结果执行successfulAuthentication或者unsuccessfulAuthentication,无论成功失败,一般的实现都是转发或者重定向等处理,不再细究AuthenticationSuccessHandler和AuthenticationFailureHandle。兴趣的可以研究一下其父类AbstractAuthenticationProcessingFilter过滤器。

# 4.3 AnonymousAuthenticationFilter

AnonymousAuthenticationFilter是匿名登录过滤器,它位于常用的身份认证过滤器(如UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter)之后,意味着只有在上述身份过滤器执行完毕后,SecurityContext依旧没有用户信息,AnonymousAuthenticationFilter该过滤器才会有意义——基于用户一个匿名身份。
AnonymousAuthenticationFilter源码分析:

public class AnonymousAuthenticationFilter extends GenericFilterBean implements
InitializingBean {
...
public AnonymousAuthenticationFilter(String key) {
    this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
    ...
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    throws IOException, ServletException {

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        //创建匿名登录Authentication的信息
        SecurityContextHolder.getContext().setAuthentication(
                createAuthentication((HttpServletRequest) req));
                ...
    }

    chain.doFilter(req, res);
}
//创建匿名登录Authentication的信息方法
protected Authentication createAuthentication(HttpServletRequest request) {
    AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
        principal, authorities);
    auth.setDetails(authenticationDetailsSource.buildDetails(request));
    return auth;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 4.4 SecurityContextPersistenceFilter

SecurityContextPersistenceFilter的两个主要作用便是request来临时,创建SecurityContext安全上下文信息和request结束时清空SecurityContextHolder。源码后续分析。

# 小节总结

  1. AbstractAuthenticationProcessingFilter:主要处理登录
  2. FilterSecurityInterceptor:主要处理鉴权

# 总结

经过上面对核心的Component、Service、Filter分析,初步了解了Spring Security工作原理以及认证和授权工作流程。Spring Security认证和授权还有很多负责的过程需要深入了解,所以下次会对认证模块和授权模块进行更具体工作流程分析以及案例呈现。最后以上纯粹个人结合博客和官方文档总结,如有错请指出!