一、自定义登录页面

默认登录页面机制

Spring Security的默认配置没有明确设定登录页面URL,因此会根据启用的功能自动生成一个登录页面URL。尽管自动生成的登录页面很方便,但大多数应用程序都希望定义自己的登录页面

自定义登录页面配置

核心配置方法

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    // 对密码进行编码, 使用不加密的对比
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    // 配置安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/r/**").authenticated() // 对所有/r/**请求必须认证通过,才可以访问
                .anyRequest().permitAll() // 除了/r/**, 其他请求都可以访问
                .and()
                .formLogin()
	                .loginPage("/login.html")          // 自定义登录页面
			        .loginProcessingUrl("/login")      // 登录处理URL
			        .usernameParameter("username")     // 用户名参数名
			        .passwordParameter("password")     // 密码参数名
			        .successForwardUrl("/index")       // 登录成功跳转页面
			        .failureForwardUrl("/login.html"); // 登录失败跳转页面
    }
}

关键配置项

  • loginPage()指定自定义登录页面路径
  • loginProcessingUrl()指定登录表单提交的处理URL
  • successForwardUrl()登录成功后的跳转页面
  • failureForwardUrl()登录失败后的跳转页面

注意事项

  • 自定义登录页面需要允许匿名访问
  • 登录页面的表单action必须与loginProcessingUrl()配置一致
  • 用户名和密码的input name必须与配置的参数名一致

二、连接数据库认证

核心概念

前边的例子我们是将用户信息存储在内存中,实际项目中用户信息存储在数据库中。根据前边对认证流程研究,只需要重新定义UserDetailService即可实现根据用户账号查询数据库。

实现步骤

1. 添加数据库依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

2. 创建用户实体类

@Entity
@Table(name = "users")
public class User {
    @Id
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
    // getter/setter...
}

3. 创建用户权限实体类

@Entity
@Table(name = "user_authorities")
public class UserAuthority {
    @Id
    private String userId;
    private String authority;
    // getter/setter...
}

4. 自定义UserDetailsService

@Service
public class SpringDataUserDetailsService implements UserDetailsService {
    
    @Autowired
    UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名查询用户信息
        User user = userRepository.findByUsername(username);
        if (user == null) {
            return null;
        }
        
        // 查询用户权限
        List<String> permissions = userRepository.findPermissionsByUserId(user.getId());
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        
        // 创建UserDetails对象
        UserDetails userDetails = User.withUsername(user.getUsername())
                .password(user.getPassword())
                .authorities(permissionArray)
                .build();
        return userDetails;
    }
}

核心要点

  • 只需要实现UserDetailsService接口
  • Spring Security会自动调用loadUserByUsername方法
  • 返回的UserDetails对象包含用户名、密码和权限信息

三、会话 (SecurityContextHolder上下文)

用户认证通过后,为了避免用户的每次操作都进行认证,可将用户的信息保存在会话中。spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext当前线程进行绑定,方便获取用户身份。

SecurityContextHolder结构

获取当前用户信息

// 获取当前认证用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

SecurityContextHolder三种存储模式

  • MODE_THREADLOCAL默认模式,SecurityContext存储在ThreadLocal中
  • MODE_INHERITABLETHREADLOCALSecurityContext存储在InheritableThreadLocal中,子线程可以获取父线程的SecurityContext
  • MODE_GLOBALSecurityContext在所有线程中都相同

会话控制

会话超时配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement()
        .maximumSessions(1)                    // 最大会话数
        .maxSessionsPreventsLogin(false)       // 达到最大会话数时是否阻止新的登录请求
        .sessionRegistry(sessionRegistry())    // 会话注册表
        .and()
        .sessionFixation().migrateSession()    // 会话固定攻击保护
        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); // 会话创建策略
}

会话创建策略

  • ALWAYS总是创建HttpSession
  • IF_REQUIREDSpring Security需要时创建HttpSession(默认)
  • NEVERSpring Security不会创建HttpSession,但如果应用创建了会使用它
  • STATELESSSpring Security不会创建HttpSession,也不会使用它

四、注销功能

注销配置

基本注销配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.logout()
        .logoutUrl("/logout")                    // 注销处理URL
        .logoutSuccessUrl("/login?logout")       // 注销成功后跳转页面
        .deleteCookies("JSESSIONID")             // 删除指定的cookie
        .invalidateHttpSession(true)             // 使HttpSession失效
        .clearAuthentication(true);              // 清除认证信息
}

注销处理器配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.logout()
        .logoutUrl("/logout")
        .addLogoutHandler(new SecurityContextLogoutHandler())  // 添加注销处理器
        .logoutSuccessHandler(new SimpleUrlLogoutSuccessHandler()); // 自定义注销成功处理器
}

核心要点

  • logoutUrl()指定注销请求的URL
  • logoutSuccessUrl()注销成功后的跳转页面
  • invalidateHttpSession(true)使当前HttpSession失效
  • clearAuthentication(true)清除SecurityContextHolder中的认证信息
  • deleteCookies()删除指定的cookie

五、授权

授权概念

授权用户认证通过后,根据用户的权限来控制用户访问资源的过程

授权实现方式

1. 基于URL的授权

通过配置URL路径和所需权限的映射关系来控制访问:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("ADMIN")     // 管理员权限
        .antMatchers("/user/**").hasRole("USER")       // 用户权限
        .antMatchers("/public/**").permitAll()         // 公开访问
        .anyRequest().authenticated();                 // 其他请求需要认证
}

2. 基于表达式的授权

使用SpEL表达式进行更复杂的权限控制:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/**").access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')")
        .antMatchers("/user/**").access("hasRole('USER') or hasRole('ADMIN')")
        .anyRequest().authenticated();
}

常用权限表达式

  • hasRole('ROLE')检查用户是否具有指定角色
  • hasAuthority('AUTHORITY')检查用户是否具有指定权限
  • hasAnyRole('ROLE1', 'ROLE2')检查用户是否具有任意一个指定角色
  • hasAnyAuthority('AUTH1', 'AUTH2')检查用户是否具有任意一个指定权限
  • permitAll()允许所有用户访问
  • denyAll()拒绝所有用户访问
  • authenticated()要求用户已认证
  • anonymous()允许匿名用户访问

5.1 web授权

配置Web授权

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/r/r1").hasAuthority("p1")
        .antMatchers("/r/r2").hasAuthority("p2")
        .antMatchers("/r/**").authenticated()    // 所有/r/**的请求必须认证通过
        .anyRequest().permitAll()               // 除了/r/**,其它的请求可以访问
        .and()
        .formLogin()                           // 允许表单登录
        .successForwardUrl("/login-success");  // 自定义登录成功的页面地址
}

权限控制规则

  • antMatchers()匹配特定的URL路径
  • hasAuthority()要求用户具有特定权限
  • authenticated()要求用户已认证
  • permitAll()允许所有用户访问

匹配器类型

  • antMatchers("/admin/**")Ant风格路径匹配
  • regexMatchers(".*[.]js")正则表达式匹配
  • mvcMatchers("/api/**")Spring MVC路径匹配

5.2 方法授权 (@PreAuthorize、@PostAuthorize、@Secured)

启用方法级安全

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    // 配置类
}

1. @PreAuthorize(方法执行前验证)

在方法执行前进行权限验证:

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(String userId) {
    // 只有ADMIN角色才能执行
}

@PreAuthorize("hasAuthority('USER_READ') or hasRole('ADMIN')")
public User getUserInfo(String userId) {
    // 需要USER_READ权限或ADMIN角色
}

@PreAuthorize("#userId == authentication.name or hasRole('ADMIN')")
public User updateUser(String userId, User user) {
    // 只能修改自己的信息或管理员可以修改任何用户
}

2. @PostAuthorize(方法执行后验证)

在方法执行后对返回结果进行权限验证:

@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public Document getDocument(String docId) {
    // 只能获取自己拥有的文档或管理员可以获取任何文档
}

@PostAuthorize("hasPermission(returnObject, 'read')")
public User getUserById(String userId) {
    // 对返回的用户对象进行读取权限验证
}

3. @Secured(简单角色验证)

基于角色的简单权限验证:

@Secured("ROLE_ADMIN")
public void adminOperation() {
    // 只有ADMIN角色才能执行
}

@Secured({"ROLE_USER", "ROLE_ADMIN"})
public void userOperation() {
    // USER或ADMIN角色都可以执行
}

4. @PreFilter和@PostFilter(集合过滤)

@PreFilter("hasPermission(filterObject, 'delete')")
public void deleteUsers(List<User> users) {
    // 过滤掉没有删除权限的用户
}

@PostFilter("hasPermission(filterObject, 'read')")
public List<Document> getDocuments() {
    // 过滤掉没有读取权限的文档
}

核心要点

  • @PreAuthorize方法执行前验证,支持SpEL表达式
  • @PostAuthorize方法执行后验证,可以访问返回值
  • @Secured简单的角色验证,不支持表达式
  • @PreFilter/@PostFilter对集合参数或返回值进行过滤
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐