跳至主要内容

Spring Security整合进Spring Boot中的基础用法

Welcome file

简介

Spring Security为应用程序提供了方便的登陆和鉴权的 API ,默认启用各项安全配置,能够简化大量的应用程序安全需求开发。不过由于这些默认启用的各项安全配置,也会让第一次集成Spring Security的开发人员有一定的上手难度。

使用环境:Java 11 和 Spring Boot 2.3.5.RELEASE

集成Spring Security

在Spring Boot中集成Spring Security与集成其他的框架类似,在pom.xml里引入相关的starter就可以:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

由于大部分的使用场景还是web,所以这里也引进Spring MVC做示例。

现在建立一个Controller作为简单的示例:

@RestController("foods")
public class FoodController {

    private List<String> foods = List.of("Bread","Sandwich");

    @GetMapping
    public List<String> getFood(){
        return foods;
    }

    @PostMapping
    public void addFood(String food){
        foods.add(food);
    }

    @DeleteMapping
    public void deleteFood(String food){
        boolean removed = foods.remove(food);
        if (!removed) {
            throw new IllegalArgumentException("no such food : " + food);
        }
    }

}

这时启动项目,会看到在控制台会有类似输出:

2020-11-26 23:27:23.376  INFO 5264 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: f091d11a-2524-40bf-b29d-a6579f6b7a5a

这里输出的是随机密码,并且访问http://localhost:8080/foods时,会进入一个页面要求输入账号和密码,默认账号是user,密码就是控制台输出的这串随机密码,填上后就能看到输出了:

["Bread","Sandwich"]

可以看到集成Spring Security后,默认就有了登陆功能。但是大多数项目不会只有一个用户,也不会使用默认的简洁的页面,所以下面介绍如何从数据库中加载用户,如何替换默认登陆页面。

Authentication

Authentication(Who are you) 就是证明你拥有这个账户,也就是通常所说的登陆流程。

配置模板

Spring Security可以帮助简化登陆流程,先需要配置对应的Spring Security配置,配置模板通常如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}
  • 为类加上@Configuration@EnableWebSecurity注解,表示为Spring Security的配置类
  • 继承WebSecurityConfigurerAdapter,覆盖需要重写的方法

配置数据库和相关实体

通常的表单登陆,都是从数据库中查询对应用户名和密码,匹配正确就登陆成功。这里建立一个简单的示例表和数据:

create table t_user
(
	id bigint auto_increment
		primary key,
	username varchar(255) not null,
	password varchar(255) not null,
	permission varchar(255) null
);

-- 密码为BCrypt哈希后的结果,密码是admin
INSERT INTO spring_security_demos.t_user (id, username, password, permission) VALUES (1, 'admin', '$2y$12$dsYcIyVxO5aOtfybzsuUr.Gc8gK8DdcqaRPr.pg6UcYODnhYET1RC', 'FOODS_VIEW,FOODS_EDIT');

再引入spring data jdbcmysql的驱动来连接数据库:

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

再创建对应的repository和entity,用于数据库操作和映射数据库表:

@Repository
public interface UserRepository extends CrudRepository<User, Long> {

    Optional<User> findByUsername(String username);

}
@Table("t_user")
public class User {

    @Id
    private Long id;

    private String username;

    private String password;
    
    private String permission;

    // GETTER SETTER
}

实现Spring Security的登陆相关接口

Spring Security有几个接口来定义登陆的相关操作,这里介绍常用的UserDetailsUserDetailsService接口。

UserDetailsService接口只有一个loadUserByUsername方法,表示从数据库或某些地方通过 username 加载用户信息,然后包装成UserDetails返回。

现在先来简单实现UserDetails接口:

public class UserDetail implements UserDetails {

    private final String username;

    private final String password;

    private Set<SimpleGrantedAuthority> authorities = Collections.emptySet();

    public UserDetail(String username, String password) {
        this.username = username;
        this.password = password;
    }

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

    @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 true;
    }

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

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

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

再来实现UserDetailsService接口:

public class JdbcUserDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    public JdbcUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("user can't found by username : " + username));

        return new UserDetail(user.getUsername(), user.getPassword());
    }

}

密码Hash算法

Spring Security在加载用户之后,会对提交的密码进行Hash后,再对比UserDetails中的密码。可以在配置中指定密码的Hash算法,用法在下面的配置中演示。

配置登陆接口

实现了登陆相关接口后,需要配置到上面说到的配置模板中,使这些实现能接入到Spring Security。配置如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        return new JdbcUserDetailsService(userRepository);
    }

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

}

到这里,主要登陆相关的配置就完成了。

这时启动程序后,在浏览器中输入:http://localhost:8080/foods,这时会看到Spring Security默认的登录页,输入admin/admin登陆后,就可以看到接口的响应了:

["Bread","Sandwich"]

自定义登陆页面

想要自定义登陆页面,可以在SecurityConfig配置中覆盖方法:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .anyRequest().authenticated()
        .and().httpBasic()
        .and().formLogin()
            .loginPage("/login.html")
            .defaultSuccessUrl("/foods",true)
            .loginProcessingUrl("/login").permitAll()
        .and().csrf().disable();
}
  • loginPage方法接收登陆页地址
  • defaultSuccessUrl方法接收登陆成功之后的跳转地址
  • loginProcessingUrl方法指定处理验证登陆的方法
  • permitAll()表示这些登陆相关方法不需要登陆就能调用,如果漏了这个,会导致循环重定向

Authorization

Authorization(Access Control)就是鉴权,判断某个用户是否有权限进行某些操作。

可以看到UserDetail中预留的authorities集合就是该用户拥有权限的集合,数据库中的permission列就是权限的集合,用逗号隔开。实际的项目中,一般是通过用户赋予的角色,再关联查出所有的权限KEY。

JdbcUserDetailsServiceloadUserByUsername稍微修改一下,把权限加载进UserDetail中:

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("user can't found by username : " + username));

        Set<SimpleGrantedAuthority> permissions = Arrays.stream(user.getPermission().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());

        return new UserDetail(user.getUsername(), user.getPassword(), permissions);
    }

配置权限验证注解

再上面提到的SecurityConfig类中,加上@EnableGlobalMethodSecurity注解,让Spring Security开启注解权限验证:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
			...
}

开启之后,再在Controller的对应方法上,加上对应的注解来为方法增加权限验证,以上面的FoodController为例:

    @GetMapping
    @PreAuthorize("hasAuthority('FOODS_VIEW')")
    public List<String> getFood(){
        return foods;
    }

    @PostMapping
    @PreAuthorize("hasAuthority('FOODS_EDIT')")
    public void addFood(String food){
        foods.add(food);
    }

    @DeleteMapping
    @PreAuthorize("hasAuthority('FOODS_EDIT')")
    public void deleteFood(String food){
        boolean removed = foods.remove(food);
        if (!removed) {
            throw new IllegalArgumentException("no such food : " + food);
        }
    }

此时可以重复登陆,并调整t_user表的permission列查看效果。比如,如果permission列中没有FOODS_VIEW权限,那么访问该接口的时候,接口会返回一个403错误,表示没有权限访问该接口。

总结

本文主要总结了Spring Security整合Spring Boot的基本用法,示例工程在Github上。

参考资料

[1]https://www.marcobehler.com/guides/spring-security

[2]官方网站:https://spring.io/projects/spring-security

评论

此博客中的热门博文

国密SM2签名封装成PKCS7格式

在国内做金融行业,难免会有被强制使用国密算法的情况,而且一般还会指定必须使用硬件加密机之类的设备,所以我也稍微的研究了一下国密算法,使用软算法签名并封装 PKCS7 格式(文档中的一个交互)。 以下是基于 Bouncy Castle 的示例,密钥对的生成可以参考 Bouncy Castle 中 test 包下 SM2 相关代码 public static String sign ( ) throws Exception { //加载公钥 byte [ ] plainText = "hello, world" . getBytes ( ) ; FileInputStream input = new FileInputStream ( "F:\\certificate\\public.cer" ) ; CertificateFactory certificateFactory = new CertificateFactory ( ) ; X509Certificate certificate = ( X509Certificate ) certificateFactory . engineGenerateCertificate ( input ) ; input . close ( ) ; //加载私钥,private为换成实际的私钥 PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec ( "private" . getBytes ( ) ) ; //SM2算法实际上为ECC算法,并指定了一些参数值,所以这里的参数是EC KeyFactory factory = KeyFactory . getInstance ( "EC" , "BC" ) ; PrivateKey privateKey = factory . generatePrivate ( spec ) ; //以下为签名并封装成PKCS7格式 byte [ ] signedMessag

Spring Boot Actuator 2 示例

Welcome file 简介 Spring Boot Actuator为应用程序提供了各种开箱即用的运维特性,可以与应用方便的交互和监控。 使用环境:Java 11 和 Spring Boot 2.4.3.RELEASE 集成Spring Boot Actuator 在Spring Boot中集成Spring Boot Actuator与集成其他的框架类似,在 pom.xml 里引入相关的starter就可以: < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-actuator </ artifactId > </ dependency > < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-web </ artifactId > </ dependency > 由于大部分的使用场景还是web,所以这里也用Spring MVC做示例。 配置好 pom.xml 后,默认actuator仅暴露一些基本功能,实际使用中,根据需求暴露对应功能。为了简便测试,这里在 application.yml 中配置暴露全部功能: management : endpoints : web : exposure : include : "*" endpoint : health : enabled : true show-details : always probes : enabled : true shutdown : enabled : true metr

NextCloud数据目录迁移

最近服务器的环境坏了,所以迁移了NextCloud的数据目录。不过在迁移过程中有点小问题。 环境: Ubuntu 18.04 Docker 19.03.7 1.NextCloud页面不正常,Docker日志显示XX目录permission denied 参考了 这里 的做法,不过是把  /var/www/html/   整个目录的权限都修改为  chown -R www-data:www-data ,之后就不再报权限问题了。 2.数据库配置修改 因为NextCloud是在初始化时填的数据库连接信息,所以直接迁移数据目录的情况下,会导致应用连不到新的数据库环境。此时可以找到数据目录下的  config/config.php 文件,直接修改数据库连接配置。