Springboot学习笔记(十)——Spring Security

概述和简单安全认证

在Java Web工程中,一般使用Servlet过滤器(Filter)对请求的进行拦截,然后在Filter中通过自己的验证逻辑来决定是否放行请求。同样的,Spring Security也是基于这个原理,在进入到DispatcherServlet前就可以对SpringMVC的请求进行拦截,然后通过一定的验证,从而决定是否放行请求访问系统。

为了使用Spring Security,需要在Maven配置文件中引入对应的依赖,如下代码所示:


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

为了对请求进行拦截,Spring Security提供了过滤器DelegatingFilterProxy类给予开发者配置。在传统的web工程中,可以使用web.xml进行配置。在传统的spring全注解的方式下,只需要加入@EnableWebSecurity,就可以驱动Spring Security了。而在Spring Boot中,只需配置如上所示代码,他便会自动启动Spring Security。

一旦启用了Spring Security,Spring IoC容器就会为你创建一个名称为springSecurityFilterChain的Spring Bean。他的类型为FilterChainProxy,事实上他也实现了Filter接口,只是他是一个特殊的拦截器。在Spring Security操作的过程中他会提供Servlet过滤器DelegatingFilterProxy,这个过滤器会通过Spring Web IoC容器去获取Spring Security所自动创建的FilterChainProxy对象,这个对象上还存在一个拦截器列表(List),列表上存在用户验证的拦截器、跨站点请求伪造等拦截器,这样他就可以提供多种拦截功能。于是焦点又落在了FilterChainProxy对象上,通过它还可以注册Filter,也就是运行注册自定义的Filter来实现对应的拦截逻辑,以满足不同的需要。

在传统的Spring项目中,Spring Security的启用只需要一个注解。例如,在web过程中可以使用@EnableWebSecurity来驱动Spring Security的启动,如果属于非Web工程,可以使用@EnableGlobalAuthentication,而事实上@EnableWebSecurity已经标注了@EnableGlobalAuthentication并且依据自己的需要加入许多Web的特性。而在Spring Boot中,只要配置的security的依赖,直接启动SpringBoot的应用也会启动Spring Security,这样就可以看到如下打印随机生成密码的日志(要保证日志级别为INFO或者其以下才能看到):

密码是随机生成的,也就是每此启动密码都会不一样。请求URL,在文本框输入用户名“user”,密码为日志打出的随机密码,然后单击登录按钮,他就能跳转到请求路径,如下图所示:
Springboot学习笔记(十)——Spring Security_第1张图片

虽然登录成功,但也出现了许多问题,比如每次启动都会造成密码的不同,造成客户每次都要输入不同的密码;用户只能使用“user”,无法多样化,无法构建不同用户的不同权限;不能自定义验证的方法;登录页面不能自定义等等。不过Spring Boot的自动配置机制,还是云粗开发者很快速的修改用户名和密码。例如,在application.properties文件中加入如下代码:

#自定义用户名和密码
spring.security.user.name=myuser
spring.security.user.password=123456

有了安全配置的属性,即使没有加入@EnableWebSecurity,Spring Boot也会根据配置的项自动启动安全机制。只是这里使用用户名“myuser”和密码“123456”就可以登录了,这样就可以自定义用户和密码了,不需要在随机生成密码。出这些配置外,Spring Boot还支持如下所示的配置项:

# SECURITY (SecurityProperties)
#Spring Security过滤器排序
spring.security.filter.order=-100
#安全过滤器责任链拦截的分发类型
spring.security.filter.dispatcher-types=async,error,request
#用户名,默认值为user
spring.security.user.name=user
#用户角色
spring.security.user.password=

#SECURITY OAUTH2 CLIENT (OAuth2ClientProperties)
#OAuth提供者详细配置信息
spring.security.oauth2.client.provider.*= #
#OAth客户端登记信息
spring.security.oauth2.client.registration.*=

以上就是Spring Boot关于Spring Security可以配置的选项。在实际工作中,大部分选项无须进行配置,只需要配置少量内容即可。

使用WebSecurityConfigerAdapter自定义

过滤器DelegatingFilterProxy的拦截逻辑就是根据FilterChainProxy对象的逻辑来完成的。为了给FilterChainProxy对象加入自定义的初始化,Spring Security提供了SecurityConfigurer接口,通过它就能够实现对Spring Security的配置。只是有了这个接口还不太方便,因为他只是能够提供接口定义的功能,为了更方便,Spring对web工程还提供了专门的接口WebSecurityConfigurer,并且在这个接口的定义上提供了一个抽象类WebSecurityConfigurerAdapter。开发者通过继承他就能得到Spring Security默认的安全功能。也可以通过覆盖他提供的方法,来自定义自己的安全拦截方案。在WebSecurityConfigurerAdapter中默认存在3个方法:

    /**
     * 用来配置用户签名服务,主要是user-details机制,还可以给予用户赋予角色
     * @param auth  签名管理器构造器,用于构建用户具体权限控制
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        super.configure(auth);
    }
        
    /**
     * 用来配置Filter链
     * @param web Spring Web Security 对象
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // TODO Auto-generated method stub
        super.configure(web);
    }
    
    /**
     * 用来配置拦截保护的请求,比如什么请求放行,什么请求需要验证
     * @param http http安全请求对象
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        super.configure(http);
    }

对于使用WebSecurity参数的方法主要是配置Filter链的请求内容,可以配置Filter链忽略哪些内容。WebSecurityConfigurerAdapter提供的是空实现,也就是没有任何的配置。而对于AuthenticationManagerBuilder参数的方法,则是定义用户(user)、密码(password)和角色(role),在默认的情况下Spring不会为你创建任何的用户和密码,也就是有登录页面而没有可登录的用户。对于HttpSecurity参数的方法,则是指定用户和角色与对应URL的访问权限,也就是开发者可以通过覆盖这个方法来指定用户或者角色的访问权限。在WebSecurityConfigurerAdapter提供的验证方式下满足通过用户验证或者HTTP基本验证的任何请求,Spring Security都会放行。

自定义用户服务信息

在WebSecurityConfigurerAdapter中的方法

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        super.configure(auth);
    }

是一个用于配置用户信息的方法,在Spring Security中默认是没有任何用户配置的。而在Spring Boot中,如果没有用户的配置,他将会自动生成一个名称为user、密码通过随机生成的用户,密码则可以在日志中观察得到。但是这样就会存在各类的弊端。为了克服这些弊端,先来看自定义用户签名服务。这里主要包含内存签名服务、数据库签名服务和自定义签名服务。关于如何限定签名服务,是通过WebSecurityConfigurerAdapter中的方法configure(HttpSecurity http)来实现的。只是这里在默认情况下,所有的请求一旦通过验证就会得到放行。

1.使用内存签名服务

使用内存签名服务就是将用户的信息存放在内存中。相对而言,他比较简单,适合于测试快速环境搭建环境。如下代码所示:

//使用内存用户
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	    // TODO Auto-generated method stub
	    super.configure(auth);
	    //密码编码器
	    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
	    //使用内存存储
	    auth.inMemoryAuthentication()
	        //设置密码编码器
	        .passwordEncoder(passwordEncoder)
	        //注册用户admin,密码abc,并赋予user和admin的权限
	        .withUser("admin")
	            //可通过passwordEncoder.encode("abc")得到加密后的密码
	            .password(passwordEncoder.encode("abc"))
	            .roles("USER","ADMIN")
	        //连接方法and,开启另外一个用户的注册
	        .and()
	        //注册用户myuser,密码为123456,并赋予USER的权限
	        .withUser("myuser")
	            .password(passwordEncoder.encode("123456"))
	            .roles("USER");
	}

在Spring5的Security中都要求使用密码编码器,否则会发生异常,否则会发生异常,所以代码中首先创建了一个BCryptPasswordEncoder实例,这个类实现了PasswordEncoder接口,它采用的是单向不可逆的密码加密方式。在AuthenticationManagerBuilder中,inMemoryAuthentication方法将返回内存保存用户信息的管理器配置(InMemoryUserDetialsManagerConfiger),这样启用内存缓存的机制保存用户信息。首先通过passwordEncoder方法,设置了密码编码器,这里的withUser方法是注册用户名称,返回用户详情构造器(UserDetailsBuilder)对象,这样就能够去配置用户的信息了;password方法是设置密码,采用的是通过BCrypt加密方式加密后的密码字符串,于是用户登录就需要这个密码了;roles方法赋予角色类型,将来就可以通过这个角色名称赋予权限了。只是这个roles方法还有内涵,他实际是一个方法的简写,这个方法是authorities,使用它可以猪注册角色名称,而代码中roles方法给角色名称实际上Spring Security会加入前缀“ROLE_”;and方法则是一个连接方法,也就是开启另外一个用户的注册。通过configure(AuthenticationManagerBuilder auth)方法,可以注册两个用户:一个是admin用户,其密码为abc,他拥有ROLE_USER和ROLE_ADMIN两个角色;另一个是myuser用户,其密码为123465,它只拥有ROLE_USER一个角色。

在上面的代码中,使用了and方法作为连接。有时候这样会显得比较冗余,可以修改代码中的onfig方法,如下所示:

// 去掉and方法,使用内存用户
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        super.configure(auth);
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        // 使用内存存储
        InMemoryUserDetailsManagerConfigurer configurer = auth.inMemoryAuthentication()
                // 设置密码编码器
                .passwordEncoder(passwordEncoder);
        // 注册用户admin,密码abc,并赋予user和admin的权限
        configurer.withUser("admin").password(passwordEncoder.encode("abc")).authorities("ROLE_USER", "ROLE_ADMIN");
        // 注册用户myuser,密码为123456,并赋予USER的权限
        configurer.withUser("myuser").password(passwordEncoder.encode("123456")).authorities("ROLE_USER");
    }

这样会显得清爽一些,还是实现相同的功能,只是这里将roles方法修改为了authorities方法,所以多加入了前缀“ROLE_”。但是无论如何使用内存缓存用户信息这样的方式不是主要的方式,因为,内存空间毕竟有限,而且会占用JVM的内存空间,不过在开发和测试阶段使用这样的方式可以满足快速开发和快速测试的需求。

下面来看用户详情构造器(UserDetailsBuilder)方法简介

UserDetailsBuilder方法简介
项目类型 描述
accountExpired(boolean) 设置账号是否过期
accountLocked(boolean) 是否锁定账号
credentialsExpired(boolean) 定义凭证是否过期
disabled(boolean) 是否禁用用户
username(String) 定义用户名
authorities(GrantedAuthority...) 赋予一个或者权限
authorities(List 使用列表(List)赋予权限
password(String) 定义密码
roles(String ...) 赋予角色,会自动加入前缀“ROLE_”

 

 

 

 

 

 

 

 

 

 

 

2.使用数据库定义用户认证服务

在大部分情况下,用户的信息会存放在数据库,为此Spring Security提供了对数据库的查询访问方法来满足开发者的需要。JdbcUserDetailsManagerConfigure是一个Spring Security对于数据库配置的支持,并且他也提供了默认的SQL。只是在大部分的情况下,不会采用他默认的提供的SQL。既然涉及到数据库,就需要准备好数据库的表。

/**角色表**/
CREATE TABLE `t_role`  (
  `id` int(12) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `note` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

/**用户表**/
CREATE TABLE `t_user`  (
  `id` int(12) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `pwd` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `available` int(1) NULL DEFAULT 1 COMMENT '是否可用,1可用,0不可用',
  `note` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `user_name`(`user_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

/**用户角色表**/
CREATE TABLE `t_user_role`  (
  `id` int(12) NOT NULL AUTO_INCREMENT,
  `role_id` int(12) NOT NULL,
  `user_id` int(12) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `role_id`(`role_id`, `user_id`) USING BTREE,
  INDEX `FK_Reference_2`(`user_id`) USING BTREE,
  CONSTRAINT `FK_Reference_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FK_Reference_2` FOREIGN KEY (`user_id`) REFERENCES `t_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

可以在数据库表中插入对应的数据,然后使用Spring Security提供的数据库权限进行验证。

// 使用数据库验证
    // 注入数据源
    @Autowired
    private DataSource dataSource = null;
    // 使用用户名查询用户信息
    String pwdQuery = "select user_name,pwd,available from t_user where user_name = ?";
    // 使用用户名称查询角色信息
    String roleQuery = "select u.user_name,r.role_name from t_user u,t_role r,t_user_role ur where u.id=ur.user_id and r.id=ur.role_id and u.user_name = ?";

  /**
   * 覆盖WebSecurityConfigurerAdapter用户详情方法
   * @param auth 用户签名管理构造器
   */
 @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        super.configure(auth);
        auth.jdbcAuthentication()
            //密码编码器
            .passwordEncoder(new BCryptPasswordEncoder())
            //数据源
            .dataSource(dataSource)
            //查询用户,自动判断密码是否一致
            .usersByUsernameQuery(pwdQuery)
            //赋予权限
            .authoritiesByUsernameQuery(roleQuery);
    }

代码中首先使用@Autowired注入了数据源,这样就能够使用SQL。其次定义了两条SQL,其中pwdQuery所定义的是根据用户名查询用户信息,roleQuery是使用角色名去查询角色信息,这样就能够赋予角色。

然后看到configure方法,使用了AuthenticationManagerBuilder的jdbcAuthentication方法,这样就可以启动jdbc的方式进行验证了。passwordEncoder方法则是设置密码解码器,然后使用dataSource方法绑定注入的数据源,接着是usersByUsernameQuery方法,他通过pwdQuery所定义的SQL返回3个列,分别是用户名、密码和布尔值。这样就可以对用户名和密码进行验证了,其中布尔值是判断用户是否有效,这里返回的是available列,他存储的数据已经被约束为1和0,如果为1则用户是有效的,否则是无效用户。而authoritiesByUsernameQuery方法会用roleQuery定义的SQL通过用户名去查询角色名称,这样Spring Security就会根据查询的结果去赋予权限,如果这条SQL返回多条,就会给这个用户赋予多个角色。

但是上述代码中存在一个弊端,虽然通过BCcrypt加密的密文很难破译。但是仍旧不能避免用户使用类似“a12345”“abcdefg”这样简单的密码,如果被人截取了这些简单的密码,进行匹配,那么一些用户的密码就有可能被别人破译。为了克服这些问题,在实际的企业生产中还可能通过自己的阴匙对密码进行加密处理,而阴匙存在企业服务器上,这样即使密文被别人截取,别人也无法得到阴匙破解密文,这样就能够大大提高网站的安全性。对此Spring Security也进行了支持,只是需要使用密码编码器(Pbkdf2PasswordEncoder类)对象即可。先在application.properties中加入一个属性:

system.user.password.secret=uvwxyz

这是一个阴匙,只要拿到这个阴匙才能通过加密算法对密码进行匹配,这样破解的难度就大大增加了,就能够更加安全的保护密码信息。然后修改成如下代码:

 // 注入配置的钥匙
    @Value("${system.user.password.secret}")
    private String secret = null;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        super.configure(auth);
        auth.jdbcAuthentication(
             //密码编码器
            .passwordEncoder(new Pbkdf2PasswordEncoder(secret))
             //数据源
            .dataSource(dataSource)
             //查询用户,自动判断密码是否一致
            .usersByUsernameQuery(pwdQuery)
             //赋予权限
            .authoritiesByUsernameQuery(roleQuery);
    }

这段代码中,使用了Pbkdf2PasswordEncoder创建密码编辑器(PasswordEncoder),实际上,Spring Security还存在SCrpyPasswordEncoder和DelegatingPasswordEncoder等密码加载器,用户可以根据自己的需要去创建不同的密码编码器,甚至可以自己实现密码编码器(PasswordEncoder)接口,定义自己的编码器。而在后面通过JdbcAuthentication设置了密码编码器,这样Spring Security就会启用这个密码编码器,这样密码就更加安全了。

3.使用自定义用户认证服务

使用Redis缓存用户数据,没有就从数据库中查询用户信息。

代码实现

Dao层:
@Mapper
public interface UserDao {
    public DatabaseUser getUser(String userName);
}

@Mapper
public interface RoleDao {
    public List findRolesByUserName(String userName);
}




    



    


Service层:
public interface UserRoleService {
    public DatabaseUser getUserByName(String userName);

    public List findRolesByUserName(String userName);
}

@Service
public class UserRoleServiceImpl implements UserRoleService {
    @Autowired
    private UserDao userDao = null;
    @Autowired
    private RoleDao roleDao = null;
    
    @Override
    @Transactional
    @Cacheable(value = "redisCache",key = "'redis_user_'+#userName")
    public DatabaseUser getUserByName(String userName) {
        // TODO Auto-generated method stub
        return userDao.getUser(userName);
    }

    @Override
    @Transactional
    @Cacheable(value = "redisCache",key = "'redis_user_role_'+#userName")
    public List findRolesByUserName(String userName) {
        // TODO Auto-generated method stub
        return roleDao.findRolesByUserName(userName);
    }

}

Controller层:
@Controller
public class WelcomeController {
    @Autowired
    private UserRoleService userRoleService = null;
    
    @GetMapping("/welcome")
    public String welcome() {
        return "welcome";
    }
    
    @GetMapping("/user/details")
    @ResponseBody
    public DatabaseUser getUser(String userName) {
        return userRoleService.getUserByName(userName);
    }
    
    @GetMapping("/user/welcome")
    public String userWelcome() {
        return "welcome";
    }
    
    @GetMapping("/admin/welcome")
    public String adminWelcome() {
        return "welcome";
    }
    
    @GetMapping("/admin/welcome1")
    public String adminWelcome1() {
        return "welcome";
    }
    
    @GetMapping("/admin/welcome2")
    public String adminWelcome2() {
        return "welcome";
    }
    
    @GetMapping("/csrf/form")
    public String csrfPage() {
        return "csrf_form";
    }
    
    @PostMapping("/csrf/commit")
    @ResponseBody
    public Map csrfCommit(String name, String describe) {
        Map map = new HashMap<>();
        map.put("name", name);
        map.put("describe", describe);
        return map;
    }
}

POJO:
@Alias("dbrole")
public class DatabaseRole implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private Long id;
    private String roleName;
    private String note;
    
}

@Alias("dbuser")
public class DatabaseUser implements Serializable{

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private Long id;
    private String userName;
    private String pwd;
    private Integer available;
    private String note;
}

首先设置用户权限的方式,对于Spring Security提供了一个UserDetailsService接口,通过它就可以获取用户信息,而这个接口只有一个loadUserByUsername方法需要实现,这个方法定义返回UserDetails接口对象,就可以很快通过类似于如下代码来实现这个接口,进而获取用户信息。

/**
 * 实现UserDetailsService接口定义用户服务类
 * @author Administrator
 *
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRoleService userRoleService = null;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO Auto-generated method stub
        //获取数据库用户信息
        DatabaseUser dbuser = userRoleService.getUserByName(username);
        //获取数据库角色信息
        List roleList = userRoleService.findRolesByUserName(username);
        //将信息转换为UserDetails对象
        return changeToUser(dbuser,roleList);
    } 
    
    private UserDetails changeToUser(DatabaseUser dbUser,List roleList) {
        //权限列表
        List authoritiesList = new ArrayList();
        //赋予查询到的角色
        for (DatabaseRole role : roleList) {
            GrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleName());
            authoritiesList.add(authority);
        }
        //创建UserDetails对象,设置用户名、密码和权限
        UserDetails userDetails = new User(dbUser.getUserName(), dbUser.getPwd(), authoritiesList);
        return userDetails;
    }

}

把这个类标注为@Service,这样Spring就能够自动地扫描他为bean,然后通过自动注入了UserRoleService接口。接着是覆盖接口的loadUserByUsername方法,在这个方法中先通过UserRoleService接口查询到用户接口和对应的角色信息,然后通过changeToUser方法把它转换为一个UserDetails接口的对象,在这个changeToUser方法里,先是构建了一个权限列表,然后通过User的构造方法将用户名称和密码传递,这样这个用户详情(UserDetails)就拥有了这些信息。然后需要给认证服务注册这个UserDetailsServiceImpl。

@Autowired
    private UserDetailsService userDetailsService = null;
    /**
     * 用来配置用户签名服务,主要是user-details机制,还可以给予用户赋予角色
     * @param auth  签名管理器构造器,用于构建用户具体权限控制
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        super.configure(auth);
        auth.userDetailsService(userDetailsService).passwordEncoder(new Pbkdf2PasswordEncoder(secret));
    }

这里因为UserDetailsServiceImpl被标注为@Service,所以会被Spring的上下文扫描装配作为bean。configure方法首先声明了密码编码器,这样就能够对密码进行加密和比较了,然后再通过userDetailsService方法注册用户服务实现类,同时绑定密码的编码器,这样就能够使用加密过后的密码了。

限制请求

对于不同的用户应该鞠永不同的角色,即访问的权限不一样。例如,一个网站可能存在普通用户和管理员用户,管理员用户拥有的权限会比普通用户要大得多,所以用户给予了登录的权限之外,还需要对于不同的角色赋予不同的权限。在上述配置的用户中,继承了抽象类WebSecurityConfigurerAdapter,并覆盖了其configure(AuthenticationManagerBuilder)方法,除此之外,这个抽象类还提供了另外一个方法,那就是configure(HttpSecurity),通过它便能够实现对于不同的角色(用户)赋予不同权限的功能。

先来看configure(HttpSecurity)方法的源码:

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();
	}

从源码中可以看出,只需要通过用户认证便可以访问所有的请求地址。它还可以通过formLogin方法配置了使用Spring Security的默认登录页面和httpBasie方法启用浏览器的HTTP基础认证方式。所以在默认的情况下,只要登录了用户,一切的请求就会畅通无阻了,但这不是真实的需要,毕竟不同的用户有着不同的角色,有时候我们需要根据角色赋予权限。因此很多时候需要覆盖此方法,让不同的角色有着不同的权限。

1.配置请求路径访问权限

对于Spring Security,他允许使用Ant风格或者正则式的路径限定安全请求,如下代码展示Ant风格的路径限定。

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //限定签名后的权限
        http.
            /* ####### 第一段 ######## */
            authorizeRequests()
            //限定“/user/welcome”请求赋予角色ROLE_USER或者ROLE_ADMIN
            .antMatchers("/user/welcome","/user/details").hasAnyRole("USER","ADMIN")
            //限定“/admin/”下所有请求权限赋予角色ROLE_ADMIN
            .antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
            //其他路径允许签名后访问
            .anyRequest().permitAll()
            
            /* ####### 第二段 ######## */
            //and代表连接词
            //对于没有配置权限的其他请求允许匿名访问
            .and().anonymous()
            
            /* ####### 第三段 ######## */
            //使用spring security默认的登录页面
            .and().formLogin()
            //启动HTTP基础验证
            .and().httpBasic();
}

先看第一段,authorizeRequests方法表示设置哪些需要签名的请求,并且可以将不同的请求权限赋予不同的角色。antMatchers配置的是请求的路径,这里是使用的是Ant风格的配置,“user/welcome”“user/details”明确指定了请求的路径。接着是hasAnyRole方法,指定了角色“ROLE_USER”“ROLE_ADMIN”,指定了这些路径只能这些角色访问。对于“/admin/**”则是通配指定,只是分配了“ROLE_ADMIN”角色可以访问。注意,hasAnyRole方法会默认加入前缀“ROLE_”,而hasAuthorities得到则不会,他们都表示对应的请求路径只有用户分配了对应的角色才能访问。然后anyRequest方法是代表任意的没有限定的请求,permitAll方法则表示没有配置过权限限定的路径允许全部访问。

再看第二段,首先是and方法,代表连接词,重新加入新的权限验证规则。这里配置了anonymous方法,说明允许匿名访问没有配置过的请求。

最后是第三段,formLogin方法代表启用Spring Security默认的登录页面,httpBasic方法表示启用HTTP的Basic请求输入用户和密码。

权限方法说明
方法 含义
access(String) 参数为SpEL,如果返回true则允许访问
anonymous() 允许匿名访问
authorizeRequest() 限定通过签名的请求
anyRequest() 限定任意的请求
hasAnyRole(String...) 将访问权限赋予多个角色(角色会自动加入前缀“ROLE_”)
hasRole(String) 将访问权限赋予一个角色(角色会自动加入前缀“ROLE_”)
permitAll() 无条件允许访问
and() 连接词,并取消之前限定前提规则
httpBasic() 启用浏览器的HTTP基础验证
formLogin() 启用Spring Security默认的登录验证
not() 对其他方法的访问采取求反
fullyAuthenticated() 如果是完整验证(并非Remember-me),则允许访问
denyAll() 无条件不允许任何访问
hasIpAddress(String) 如果是给定的ip地址则允许访问
rememberme() 用户通过Remember-me功能验证就允许访问
hasAuthority(String) 如果是给定的角色就允许访问(不加入前缀“ROLE_”)
hasAnyAuthority(String...) 如果是给定的角色中的任意一个就允许访问(不加入前缀“ROLE_”)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2.使用Spring表达式配置访问权限

使用Spring EL进行配置。这就需要使用到上表中的access方法,他的参数就是一个Spring表达式,如果这个表达式返回true,就允许访问,否则就不允许访问。

Spring表达式设置权限

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        http.authorizeRequests()
        //使用spring表达式只有角色USER或者ADMIN
            .antMatchers("/user/**").access("hasRole('USER') or hasRole('ADMIN')")
            //设置访问权限给角色ROLE_ADMIN,要求是完整登录(非记住我登录)
            .antMatchers("/admin/welcome1").access("hasAuthority('ROLE_ADMIN') && idFullyAuthenticated()")
            //限定“/admin/welcome2”访问权限给角色ROLE_ADMIN,允许不完整登录
            .antMatchers("/admin/welcome2").access("hasAuthority('ROLE_ADMIN')")
            //使用记住我的功能
            .and().rememberMe()
            //使用spring security默认的登录页面
            .and().formLogin()
            //启动HTTP基础验证
            .and().httpBasic();

上述代码中使用了3个Spring表达式,借助他们来实现配置访问权限的功能。其中第一个表达式使用了hasRole方法,通过他的参数限定了角色“ROLE_USER”或“ROLE_ADMIN”才拥有访问权限;第二个表达式是使用hasAuthority方法,通过限定“ROLE_ADMIN”角色才拥有访问权,并且要求是完整登录,不再接受“记住我”(Remember Me)这样的验证方式进行访问;第三个表达式是允许“ROLE_ADMIN”角色进行访问。最后remembermMe方法是启用“记住我”功能。

除了以上代码中的正则式的方法外,Spring Security还提供了其他的方法,如下表所示:

Spring Security中的Spring表达式方法
方法 含义
authentication() 用户认证对象
denyAll() 拒绝任何访问
hasAnyRole(String...) 当前用户是否存在参数中列明的对象属性
hasRole(String) 当前用户是否存在角色
hasIpAddress(String) 是否请求来自指定的ip
isAnonymous() 算法匿名访问
isAuthenticated() 是否用户通过认证签名
isFullyAuthenticated() 是否用户完成验证,即非“记住我”(Remember Me认证)功能通过的认证
isRememberMe() 是否是通过“记住我”功能通过的验证
permitAll() 无条件允许任何访问
principal() 用户的principal对象

 

 

 

 

 

 

 

 

 

 

 

 

 

通过表中的Spring表达式就能够配置权限,从而限定请求的访问权限。

3.强制使用HTTPS

在一些实际的工作环境中,如银行、金融公司和商品购物等,对于银行账户、密码、身份信息等往往都是极为敏感的,对于这些信息往往需要更为谨慎的保护。通过HTTPS协议采用证书进行加密,对于那些敏感的信息就可以通过加密进行保护了。对于那些需要加密的页面,在Spring中可以强制使用HTTPS请求,如下代码:

http
        //使用安全渠道,限定为https请求
        .requiresChannel().antMatchers("/admin/**").requiresSecure()
        //不使用https请求
        .and().requiresChannel().antMatchers("/user/**").requiresInsecure()
        //限定允许的访问角色
        .and().authorizeRequests().antMatchers("/admin/**").hasAnyRole("ADMIN")
        .antMatchers("/user/**").hasAnyRole("ROLE","ADMIN");

这里的requiresChannel方法说明使用通道,然后antMatchers是一个限定请求,最后使用requiresSecure表示使用HTTPS请求。这样对于Ant风格下的地址/admin/**就只能使用HTTPS协议进行请求了,而对于requireInsecure则是取消安全请求的机制,这样就可以使用普遍的HTTP请求。

4.防止跨站点请求伪造

跨站点请求伪造(Cross-Site Request Forgery,CSRF)是一种常见的攻击手段。首先是浏览器请求安全网站,于是可以进行登录,在登录之后会记录一些信息,一Cookie的形式保存,然后在不关闭浏览器的情况下,用户可能访问一个危险网站,危险网站通过获取Cookie信息来仿造用户的请求,进而请求安全网站,这样就给网站带来很大的危险。

为了克服这个危险,Spring Security提供了方案来处理CSRF过滤器。在默认的情况下,他会启用这个过滤器来防止CSRF攻击。当然,也可以关闭这个功能。例如,使用代码:

http.csrf().disable().authorizeRequests()......

就可以关闭CSRF过滤器的验证了,只是这样就会给网站带来一定被攻击的风险,因此在大部分情况下,都不建议将这个功能关闭。

那么对于不关闭CSRF的Spring Security,每次HTTP请求的表单(Form)就要求存在CSRF参数。当访问表单的时候,Spring Security就生成CSRF参数,放入表单中,这样当提交表单到服务器时,就要求连同CSRF参数一并提交到服务器。Spring Security就会对CSRF参数进行判断,判断是否与其生成的保持一致。如果一致,他就不会认为该请求来自CSRF攻击;如果CSRF参数为空或者与服务器的不一致,他就认为这是一个来自CSRF的攻击而拒绝请求。因为这个参数不在Cookie中,所以第三方网站是无法伪造的,这样就可以避免CSRF攻击。

假设已经配置好路径“/csrf/form”,请求映射为“WEB-INF/jsp/csrf_form.jsp”,然后给这个jsp表单添加参数token的信息,如下代码所示:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>




Insert title here


    

名称:

描述:

注意在代码中的jstl表达式,其中_csrf对象是Spring参数提供的。当启用CSRF攻击的安全认证功能后,Spring Security机制就会生成对应的CSRF参数,他的属性parameterName代表的是名称,属性token代表的是token值。这些都会放在表单(Form)的隐藏域中,所以在提交的时候会提交到服务器后端,这时Spring Security的CSRF过滤器就会去验证这个token参数是否有效,进而可以避免CSRF攻击。

用户认证功能

1.自定义登录界面

上述的安全登录都使用Spring Security默认的登录页面,实际上,更多的时候需要的是自定义的登录页。有时候还需要一个“记住我”的功能,避免用户在自己的客户端每次都需要输入密码,关于这些,Spring Security都提供了进行管理的办法。如下代码所示,将通过覆盖WebSecurityConfigAdapter的config(HttpSecurity)方法,让登录页指向对应的请求路径和启用“记住我”(Remember Me)功能。

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //访问/admin下请求需要管理员的权限
            .authorizeRequests().antMatchers("/admin/**").access("hasRole('ADMIN')")
            //启用memberme功能
            .and().rememberMe().tokenValiditySeconds(86400).key("remember-me-key")
            //启动HTTP基础验证,设置名称
            .and().httpBasic().realmName("my-basic-name")
            //通过签名后可以访问任何请求
            .and().authorizeRequests().antMatchers("/**").permitAll()
            //设置登录页和默认的跳转路径
            .and().formLogin().loginPage("/login/page")
                .defaultSuccessUrl("/admin/welcome1");
}

这里的rememberMe方法意思为启用了“记住我”功能,这个“记住我”的有效时间为天(86400s),而在浏览器中将使用Cookie以键“remember-me-key”进行保存,只是在保存之前会议MD5加密,这样就能够在一定程度上对内容进行保护。loginPage方法是指定登录路径为“login/page”,defaultSuccessUrl方法是指定默认的跳转路径为“admin/welcome1”。

这样需要指定login/page所映射的路径,我们可以使用传统的控制器去映射,也可以使用新增的映射关系去完成,如下代码所示:

public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // TODO Auto-generated method stub
        WebMvcConfigurer.super.addViewControllers(registry);
        // 使得"/login/page"映射为"login.jsp"
        registry.addViewController("/login/page").setViewName("login");
        // 使得"/logout/page"映射为"logout_welcome.jsp"
        registry.addViewController("/logout/page").setViewName("logout_welcome");
        // 使得"/logout"映射为"logout.jsp"
        registry.addViewController("/logout").setViewName("logout");
    }
}

代码中WebConfig类实现了WebMvcConfigurer接口,并覆盖了addViewControllers方法。在方法里存在3个路径位置,比如注册的URL,即“login/page”,他映射为“login.jsp”。jsp代码如下所示:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>




Insert title here


 

名称:

描述:

记住我:

在这个表单(form)提交的action定义为“/login/page”,这里安全登录拦截器就会拦截这些参数了,这里要求method为“POST”,不能是“GET”。表单中定义用户名且要求参数名称为username,密码为password,“记住我”为remember-me,且“记住我”是一个CheckBox。这样提交到登录URL的时候,Spring Security就可以获取这些参数,只是要切记这里的参数名是不能修改的。之前Spring boot中CSRF过滤器实惠默认启动的,因此这里还会在请求表单中加入了对应的参数,这样就可以避免CSRF的攻击了。通过上面的代码,就可以实现自定义登录页面和启用“记住我”的功能。

2.启用HTTP Basic认证

HTTP Basic认证是一个浏览器的自动弹出简单的模态对话框的功能。在REST风格的网站就比较适合这样的验证,为此可以使用如下代码所示来启用他。

#启用HTTP Basic认证
http.httpBasic()
    #设置名称
    .realmName("my-basic-name");

这里的httpBasic方法的作用是启用HTTPBasic认证,而realmName方法的作用是设置模态对话框的标题。

3.登出

对于默认情况下,Spring Security会提供一个URL——“/logout”,只要使用HTTP的POST请求(GET请求是不能退出的)了这个URL,Spring Security就会登出,并且清除remember me功能保存的相关的信息。有时候也想自定义请求退出的路径。在类WebConfig中,将请求“/logout/page”映射为“logout_welcome.jsp”,作为登出后的欢迎页面,这样便需要开发logout_welcome.jsp,如下所示:
 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>




Spring Security 登出


您已经登出了系统

类WebConfig中还将请求“/logout”映射为“logout.jsp”,作为测试登出的页面如下所示:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>




登出


    

这里的表单(form)定义将提交路径设置为“/logout/page”,方法为POST(不能为GET),并且表单中还有CSRF的token参数,为了使Spring Security的LogoutFilter能够拦截这个动作的请求,需要修改WebSecurityConfigurerAdapter的方法configure(HttpSecurity),代码如下所示:

	protected void configure(HttpSecurity http) throws Exception {
	    http
	        // 访问/admin下的请求需要管理员权限
	        .authorizeRequests().antMatchers("/admin/**")
	            .access("hasRole('ADMIN')")
	        // 通过签名后可以访问任何请求
	        .and().authorizeRequests()
	            .antMatchers("/**").permitAll()
	        // 设置登录页和默认的跳转路径
	        .and().formLogin().loginPage("/login/page")
	            .defaultSuccessUrl("/admin/welcome1")
	        // 登出页面和默认跳转路径
	        .and().logout().logoutUrl("/logout/page")
	            .logoutSuccessUrl("/welcome");
	}

在最后两行代码中,定义了成功登出跳转的路径为“/welcome”,而登出的请求URL为“/logout/page”,这样使用POST方法请求“/logout/page”的时候,Spring Security的过滤器LogoutFilter就可以拦截这个请求执行登出操作了,这时他只拦截HTTP的post请求,而不拦截get请求。

你可能感兴趣的:(SpringBoot,学习笔记)