手把手带你入门 Spring Security

Spring Security 由于配置复杂,一直被人所诟病,所以对于 SSM 框架的项目来说,轻量的 Shiro 显然更适合它。然而 Spring Boot 的横空出世打破了这个局面,Spring Boot 通过自动配置,使得开发者在 Spring Boot 中使用 Spring Security 变得非常简单。现如今的 Spring Boot 应用若是想集成安全框架,基本都会毫不犹豫地选择 Spring Security。

HelloWorld 案例

首先通过一个案例来感受一下在 Spring Boot 中如何使用 Spring Security,创建一个 Spring Boot 应用,并引入依赖:



    org.springframework.boot
    spring-boot-starter-security


编写一个控制器:

@RestController
public class TestController {

    @RequestMapping("hello")
    public String hello(){
        return "Hello SpringSecurity!";
    }
}

然后直接启动项目,访问 http://localhost:8080/login:

结果打开的是一个登录页面,其实这时候我们的请求已经被保护起来了,要想访问,得先登录。

在这个案例中仅仅是引入了一个 Spring Security 的 starter 启动器,没有做任何的配置,而项目已经具有了权限认证。

现在我们来登录一下:

手把手带你入门 Spring Security

Spring Security 默认提供了一个用户名为 user 的用户,其密码在控制台可以找到:

成功登录以后就可以正常访问了:

手把手带你入门 Spring Security

用户认证

刚才的案例中我们使用的是 Spring Security 提供的用户名和密码进行登录的,那么该如何配置自己的用户名和密码呢?

按照 Spring Boot 的自动配置原理,它肯定为其编写了一个 XXXProperties 的类作为配置,来查找一下:

手把手带你入门 Spring Security

找到了这个类就知道该如何配置了:

通过这几个地方,我们能够知道一些信息,配置必须使用 spring.security 前缀,然后可以看到 Spring Security 为我们初始化的用户名和密码,所以若是想修改配置,则应使用 spring.security.user.name 和 spring.security.user.password。

在 Spring Boot 的配置文件中进行如下配置:

spring:
  security:
    user:
      name: wwj
      password: 123

此时启动项目,将只能通过自己配置的用户名和密码登录。

当然还可以通过配置类的方式进行配置,创建一个配置类继承 WebSecurityConfigurerAdapter:

@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //对密码进行加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("wwj").password(password).roles("admin");
    }
}

重新启动项目测试一下。会发现登录不上,观察控制台:

这是因为我们在对密码加密的时候使用到了 BCryptPasswordEncoder 对象,而容器中并没有这个对象,所以我们还需要创建该对象:

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

再次重新启动一切正常。

我们还可以采取自定义实现类的方式来实现,首先仍然是创建配置类:

@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
    }

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

此时我们需要实现 UserDetailsService 接口:

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        String password = new BCryptPasswordEncoder().encode("123");
        //权限集合
        List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        return new User("wwj", password, authorities);
    }
}

查询数据库完成登录认证

刚才我们对案例进行了进一步的操作,即通过自己指定的用户名和密码进行认证,然而真实的生产环境中,认证的过程肯定是要经过数据库的,用户输入用户名和密码,然后进行数据库查询验证登录,接下来就实现一下这个过程。

首先引入依赖:


    org.mybatis.spring.boot
    mybatis-spring-boot-starter
    2.1.4


    mysql
    mysql-connector-java


    org.projectlombok
    lombok


创建数据表:

create database springsecurity;

use springsecurity;

create table user(
 id int primary key auto_increment,
    username varchar(20),
    password varchar(20)
);

insert into user values(null,'zhangsan','123');
insert into user values(null,'lisi','456');

然后就可以使用 MyBatis 的逆向工程生成一下实体类、Mapper 接口和 Mapper 配置文件,之后要在 Spring Boot 的配置文件中进行 MyBatis 的相关配置:

spring:
  datasource:
    url: jdbc:mysql:///springsecurity?serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456

mybatis:
  type-aliases-package: com.wwj.springsecuritydemo.bean
  mapper-locations: classpath:mappers/*.xml

最后在启动类上添加注解:

@SpringBootApplication
@MapperScan("com.wwj.springsecuritydemo.dao")
public class SpringsecuritydemoApplication {

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

}

这样 MyBatis 就整合完成了,接下来是 Spring Security 的相关配置,还记得我们是如何实现自定义用户登录的吗?一起回忆一下吧,首先需要一个配置类:

@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
    }

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

该配置类中注入了一个 UserDetailsService 对象,它是一个接口,所以我们需要自定义类实现该接口:

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        String password = new BCryptPasswordEncoder().encode("123");
        //权限集合
        List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        return new User("wwj", password, authorities);
    }
}

之前我们是这样写的,直接返回 User 对象即可,这个 User 对象是 Spring Security 提供的,不是我们创建的实体类 User。

现在我们就需要修改这个类:

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //这里的 String s 实际上是表单传递过来的用户名
        //根据用户名查询数据表
        UserExample userExample = new UserExample();
        UserExample.Criteria criteria = userExample.createCriteria();
        criteria.andUsernameEqualTo(s);
        List userList = userMapper.selectByExample(userExample);
        if (userList == null || userList.isEmpty()) {
            //没有查询到用户,认证失败
            throw new UsernameNotFoundException("该用户不存在!");
        }
        //取出用户信息
        com.wwj.springsecuritydemo.bean.User user = userList.get(0);
        String password = new BCryptPasswordEncoder().encode(user.getPassword());
        //权限集合
        List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        return new User(user.getUsername(), password, authorities);
    }
}

首先 loadUserByUsername(String s) 方法的入参 String s 实际上是表单传递过来的用户名,然后通过该用户名在数据表中查询,若查询不到结果,说明用户不存在,抛出异常即可;若查询出了结果,则需要将用户信息封装到 Spring Security 提供的 User 对象中进行返回。

我们可以通过 Debug 的方式来具体看看执行流程,直接在第一行代码上打个断点:

手把手带你入门 Spring Security

然后以 Debug 方式启动:

手把手带你入门 Spring Security

当输入一个不存在的用户并登录时:

手把手带你入门 Spring Security

可以看到此时的 s 就是我们输入的用户名,而当我们输入一个正确的用户名时:

手把手带你入门 Spring Security

loadUserByUsername() 方法同样获取到输入的用户名,

手把手带你入门 Spring Security

如果密码输入错误也是无法进行登录的,这是因为 Spring Security 有着它自己的验证方式,因为我们目前还是用的 Spring Security 提供的登录页面,所以密码的校验也是由 Spring Security 自己完成的。

自定义登录页面

刚才我们又对案例进行了升级,现在已经可以根据数据表中的用户信息进行登录校验了,然而 Spring Security 提供的登录页面过于简单,那么该如何将其替换成我们自己的登录页面呢?

首先来到配置类,在配置类中重写 configure(HttpSecurity http) 方法:

@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .and()
                .authorizeRequests()
                .antMatchers("/","/hello","/user/login")//配置哪些路径可以直接访问
                .permitAll()
                .anyRequest().authenticated()//拦截所有资源
                .and()
                .formLogin()
                .loginPage("/login.html")//设置登录页面
                .loginProcessingUrl("/user/login")//设置登录的请求路径
                .defaultSuccessUrl("/user/index")//设置登录成功后的跳转路径
                .permitAll()
                .and()
                .csrf().disable();//禁用 csrf
    }

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

此时就可以通过 configure(HttpSecurity http) 方法的入参 http 进行相关的设置,需要注意的是,其中 loginProcessingUrl 方法设置的是登录的请求路径,即登录表单的 action 属性需要与其对应,登录表单如下:




    
    Title


    
用户名:
密码:

这里注意了,表单中的用户名和密码输入框的 name 属性值必须为 username 和 password,否则 Spring Security 就无法获取到这两个参数,也就无法帮助你完成登录校验了。

最后编写几个控制方法进行测试:

@RestController
public class TestController {

    @RequestMapping("/hello")
    public String hello(){
        return "Hello SpringSecurity!";
    }

    @GetMapping("/user/index")
    public String index(){
        return "Hello Index!";
    }

    @GetMapping("/user/test")
    public String test(){
        return "Test!";
    }
}

此时启动项目,我们可以直接来访问 http://localhost:8080/hello:

访问成功,这是因为我们配置了 /hello 请求可以直接访问,那么接下来测试一下 http://localhost:8080/user/index:

手把手带你入门 Spring Security

可以看到,因为 /user/index 是被保护的,所以 Spring Security 帮助我们跳转到了登录页面,此时我们进行登录即可,登录成功后就能正常访问了:

手把手带你入门 Spring Security

若是直接访问登录页面:

手把手带你入门 Spring Security

则登录后会跳转至 defaultSuccessUrl 方法配置的请求路径中。

基于权限访问控制

前面我们已经实现了资源的访问保护,然而并不是所有登录认证通过后的用户都可以访问系统中的所有资源,我们应该对用户进行权限的划分,比如划分为普通管理员和超级管理员权限,那么普通管理员能够操作的资源就肯定要少于超级管理员。

接下来就来看看在 Spring Security 中是如何实现权限访问控制的:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .and()
        .authorizeRequests()
        .antMatchers("/","/hello")
        .permitAll()
        //设置权限,当前用户必须有 admin 权限才能访问该路径
        .antMatchers("/user/index").hasAuthority("admin")
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .loginPage("/login.html")
        .loginProcessingUrl("/user/login")
        .defaultSuccessUrl("/user/index")
        .permitAll()
        .and()
        .csrf().disable();
}

其实非常简单,在原来的配置基础上添加 hasAuthority 方法,该方法会判断用户是否拥有指定的权限,此时表示 /user/index 请求必须拥有 admin 权限才能够访问,直接启动项目测试一下:

手把手带你入门 Spring Security

可以看到我们是能够直接登录成功的,这是因为在 MyUserDetailsService 中我们为每个用户都设置了 admin 权限:

手把手带你入门 Spring Security

下面我们修改一下权限:

List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ord");

重新启动项目:

手把手带你入门 Spring Security

我们可以看到网页报错了,显示的是 403,即无权限访问,当然了,这个页面我们也是可以进行设置了。

有时候的一些资源是可以提供多个权限的用户访问的,这时我们就需要使用 hasAnyAuthoirty 方法为请求路径设置多个权限:

//设置权限,当前用户必须有 admin 权限才能访问该路径
.antMatchers("/user/test").hasAnyAuthority("admin,manager")

此时 admin 和 manager 权限的用户均可以访问 /user/test 请求。

基于角色访问控制

角色与权限类似,但又有些许不同,通常在一个系统中,权限不会直接分配给用户,而是指定用户为某个角色或某些角色,并由这些角色来决定用户具有哪些权限。比如在一个服装后台系统中,作为销售角色的用户,它就只有浏览衣服库存和价钱的权限。

而在 Spring Security 中,可以使用 hasRole 方法为某个请求设置角色访问控制:

//设置角色,当前用户必须为 sale 角色才能访问该路径
.antMatchers("/user/test").hasRole("sale")

此时表示 /user/test 请求只有 sale 角色的用户才能访问,在 MyUserDetailsService 中进行设置:

List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");

需要注意的是 hasRole 方法底层会为我们的角色名拼接一个 ROLE_ 前缀,所以在为用户设置角色时需要加上该前缀:

private static String hasRole(String role) {
    Assert.notNull(role, "role cannot be null");
    if (role.startsWith("ROLE_")) {
        throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
    } else {
        return "hasRole('ROLE_" + role + "')";
    }
}

它同样也可以设置多个角色,使用 hasAnyRole 方法即可,用法与 hasAnyAuthority 类似。

自定义权限不足页面

在前面我们实现了基于权限和角色的访问控制,当权限不足时,页面会显示 403,这种错误对用户来说是不友好的,为此,我们应该自定义该页面,并让其在权限不足,无法访问时跳转至我们自己的页面。

实现非常简单,直接在 configure 方法中进行配置即可:

//配置 403 页面
http.exceptionHandling().accessDeniedPage("/403.html");

编写一个页面:




    
    Title


    

您无权访问!

手把手带你入门 Spring Security

注解的使用

Spring Security 还支持注解的方式配置,下面介绍常用的五个注解:

  • @Secured
  • @PreAuthorize
  • @PostAuthorize
  • @PreFilter
  • @PostFilter

@Secured 注解用于判断用户是否为某个角色,注意这里也要加上 ROLE_ 前缀,使用该注解前需要在启动类上添加一个注解:

@SpringBootApplication
@MapperScan("com.wwj.springsecuritydemo.dao")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecuritydemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringsecuritydemoApplication.class, args);
    }
}

此时在控制方法上添加该注解即可:

@GetMapping("/testSecured")
@Secured({"ROLE_sale","ROLE_manager"})
public String testSecured(){
    return "testSecured";
}

这里表示只有用户为 sale 或 manager 角色时才能访问 /testSecured 请求。

@PreAuthorize 注解适合进入方法前的权限验证,使用该注解前需要在启动上添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解:

@GetMapping("/testPreAuthorize")
@PreAuthorize("hasAnyAuthority('admin')")
public String testPreAuthorize() {
    return "testSecured";
}

@PostAuthorize 注解会在方法执行后再进行权限验证,适合带有返回值的权限,它与 @PreAuthorize 的用法类似,加上不太常用,这里就不做介绍了。

用户注销

登录认证之后,自然要有注销功能,否则当用户准备退出系统时会发现自己无法做到退出,导致一些安全问题。

只需在配置类中添加如下配置即可:

//用户注销
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();

此时已经完成用户注销功能,但为了方便测试,这里先创建一个登录成功后跳转的页面 success.html:




    
    Title


    

登录成功!

注销

注销超链接的 href 属性需要填写为在配置类中配置的 logoutUrl 属性值。

然后在配置类中将登录成功后的跳转 url 设置为该页面:

.defaultSuccessUrl("/success.html")//设置登录成功后的跳转路径

测试一下:

手把手带你入门 Spring Security

好了,以上就是本篇文章的全部内容了,希望对你入门有帮助吧!如有错误或未考虑完全的地方,望不吝赐教!

你可能感兴趣的:(手把手带你入门 Spring Security)