Loading

Spring实战 九 保护Web应用

Spring中提供权限认证的模块就是spring-security,先导入。

现在dependencyManagement中写入

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-bom</artifactId>
            <version>${spring.security.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

再在dependencies中定义

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

spring-security提供了基于Filter链实现的基于请求的安全保护,也通过AOP实现了方法级别的保护。

它定义了一套Filter链对登录的各个过程进行校验,但这些都不用我们管,我们要做的就是在java中提供一些基础的配置即可。

简单介绍Spring Security的原理

Spring Security通过标准的Servlet规范中的Filter来实现安全保护。

就像DispatcherServlet一样,Spring也有一个DelegatingFilterProxy作为接收所有请求的前端Filter,DelegatingFilterProxy维护一个FilterChain并在接到请求时调用它们进行各种各样的安全校验。

DelegatingFilterProxy有一个好处就是它连接了Servlet上下文和Spring应用上下文,让Spring上下文中通过Bean注册的Filter能够被应用扫描到。Servlet并不具备扫描Bean的功能。

这是Spring官方的一张图,Spring的DelegatingFilterProxy与其它的Servlet和Filter同在Servlet的FilterChain中,而DelegatingFilterProxy自己又维护了一个FilterChain,DelegatingFilterProxy会委托这些链中的Filter进行安全校验。

使用Java配置SpringSecurity

创建两个类

public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {

}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
}

AbstractSecurityWebApplicationInitializer用于初始化DelegatingFilterProxy和默认的FilterChain,它实现自WebApplicationInitializer所以它的初始化方法能够在Servlet容器时被调用。

SecurityConfig用于我们来配置一些安全策略,现在我们什么也没配置,默认的策略要求应用中的所有请求都必须通过用户验证,但是我们目前又没有用户,这不是尴尬了??

配置认证管理

我们通过SecurityConfig中的config方法来配置一些用户,有三个config方法,分别用于不同的配置,我们现在选择具有AuthenticationManagerBuilder参数类型的,这个方法用于进行用户认证管理相关的配置,我们可以在这个方法中指定从哪里存储,如何存储用户信息。

以下是三个config方法的作用

inMemoryAuthentication

对于测试环境,Spring提供了一套很方便的inMemoryAuthentication认证管理方式,通过inMemoryAuthentication,我们可以在内存中创建一些用于开发环境的用户。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .passwordEncoder(new BCryptPasswordEncoder())
            .withUser("user")
                .password(new BCryptPasswordEncoder().encode("user"))
                .roles("USER").and()
            .withUser("admin")
                .password(new BCryptPasswordEncoder().encode("admin"))
                .roles("USER","ADMIN");
}

如上,AuthenticationManagerBuilder的建造者模式让我们很方便的指定这些用户信息,我们先用了inMemoryAuthentication来指定了使用基于内存的认证管理方式,然后我们传入了一个passwordEncoder指定如何对密码信息进行加密,再然后就是使用withUser在内存中定义一个用户,并使用password定义密码,注意这里传入的密码需要是加密过的,并且加密方式要和前面指定的一样,不然登陆的时候的加密方式和你这里存储的加密方式不一样,得到的密文不一样,就登陆不进去。最后使用role来指定用户的角色。and可以将两个配置连接起来。

rolesauthorities方法的简写形式,因为Spring中的角色以ROLE_开头,如果按原始写法,应该是这样:

但是我们使用role方法,会自动在前面添加ROLE_前缀。

UserDetialsService

Spring也提供了基于数据库,基于LDAP的认证管理方式,但是我们这里先不研究,研究一个更加通用的——UserDetialsService

创建一个类,实现UserDetialsService,实现loadUserByUsername方法,返回一个用户信息,这是Spring与UserDetialsService唯一的约定,至于你UserDetialsService中基于内存管理还是数据库管理还是其他方法,那就和Spring没关系了。

我们这里使用之前的Spittr应用,将Spittr的用户系统通过UserDetialsService集成到Spring Security中

我们的SpitterUserService将通过用户名加载用户的实现委托给了之前的SpitterRepository,Service实际没处理啥,就是获取结果,然后添加权限,构造用户对象,返回,或在没找到对象时抛出异常。

public class SpitterUserService implements UserDetailsService {

    private final SpitterRepository repository;

    public SpitterUserService(SpitterRepository repository) {
        this.repository = repository;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Spitter spitter = repository.getOneByUserName(s);
        if (spitter!=null) {
            List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority("ROLE_SPITTER"));

            return new User(spitter.getUserName(),
                    new BCryptPasswordEncoder().encode(spitter.getPassword()),
                    authorities);
        }
        throw new UsernameNotFoundException("User " + s + " not found!");
    }

}

需要注意的是,这里我们也使用了BCryptPasswordEncoder对密码进行加密,如果使用明文密码,可以在密码前面连接{noop}字符串,这大括号中是加密方式,指定为noop后Spring就不会对其加密了。

还有持久层中原来没有getOneByUserName方法,我这里补上。

@Override
public Spitter getOneByUserName(String username) {
    return spitterStorage.values().stream()
        .filter(spitter -> spitter.getUserName().equals(username))
        .findFirst().get();
}

然后,我们需要在SecurityConfig中配置这个SpitterService

@Autowired
private SpitterRepository repository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(new SpitterUserService(repository))
            .passwordEncoder(new BCryptPasswordEncoder());
}

自动注入SpitterRepository,使用userDetailsService指定通过UserDetialsService进行认证,并将我们的实现类SpitterUserService传入,而且要指定一致的passwordEncoder

现在就将我们Spittr应用中自己的用户系统接入到Spring中了。

拦截请求

应用中有一部分页面是不需要认证的,比如自定义的登录页面。对于Spittr应用,首页不需要认证,Spittle列表页不需要认证。

其实需要认证的只有当用户查看“我”页面时需要他已经登陆,已经拥有一个Spitter身份,再有就是当用户发表Spittle时,需要有一个Spitter身份。

通过实现参数为HttpSecurity的方法来对不同的url应用不同的安全策略。下面对调用authorizeRequests返回的对象调用antMatchers来使用ant语法匹配url,还有regexMatchers使用正则表达式来匹配,authenticated方法要求指定路径下的用户必须已经通过验证。使用这两个方法的组合,我们对/spitters/me页面和POST请求的/spittles页面进行了限制,要求访问的用户必须已经通过验证。对于其它的url,使用anyRequest().permitAll()来对所有请求允许访问。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/spitters/me").authenticated()
            .antMatchers(HttpMethod.POST,"/spittles").authenticated()
            .anyRequest().permitAll();
}

上面的方法中,antMatchersanyRequest用来匹配url,authenticatedpermitAll用来指定如何保护匹配的请求。Spring提供了如下的保护机制

下面我们改写代码,把authenticated改为需要拥有ROLE_SPITTER角色才能够正常访问。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/spitters/me").hasRole("SPITTER")
            .antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
            .anyRequest().permitAll();
}

使用了hasRole来指定访问的用户必须拥有SPITTER角色,当然也可以通过hasAuthority("ROLE_SPITTER")来访问。

使用SpEL

access方法可以执行给定的SpEL表达式来进行权限判断,并且提供了差不多的扩展表达式语法

比如我们想对用户角色和IP地址同时进行限制,可以

antMatchers("/spitters/me").access("hasRole('SPITTER') and hasIpAddress('192.168.1.2')");

强制通道安全性

现在默认都是https了,https在传输时对请求进行加密,被恶意窃取的机会更小了。

HttpSecurity除了authorizeRequests方法还有一个requiresChannel方法来指定请求的通道。

下面使用了requiresChannel来对指定的url进行通道安全性配置,指定了/spitter/form路径必须经过安全的通道,也就是https。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
                .antMatchers("/spitters/me").hasRole("SPITTER")
                .antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
                .anyRequest().permitAll()
            .and()
            .requiresChannel()
                .antMatchers("/spitter/form").requiresSecure();
}

也可以使用requiresInsecure来指定只使用http。

csrf

CSRF攻击是一种常见的网络攻击形式,原理就是黑客编写恶意的网页,伪造表单来向其他服务器发送请求,并诱导用户点击提交按钮。比如:

假如这是黑客编写的恶意表单,message字段是隐藏的,如果你点击按钮,并且你使用过Spittr应用,浏览器中还存在Cookie的话,你就会在Spittr应用中发送一条I'm stupid!的消息。

想想如果这个表单是转账或者其它的重要表单,造成的后果不可想象。

Spring Security使用同步token实现防csrf攻击,服务器端通过Cookie给客户端发放一个csrf token并设置httponly(这样客户端不能通过js来获取cookie内容),客户端下次请求时不在Cookie中携带这个token,而是通过将这个token添加到请求参数中(因为Cookie会自动携带,而请求参数必须主动携带,这样黑客就没办法进行csrf攻击)

这意味着我们的前端页面的表单或ajax中就必须有一个携带csrf token的域,这个域就是_csrf

使用Thymeleaf模板和jsp的sf:form标签会自动生成这个隐藏域,如果使用传统jsp,则可以按如下方法

<input type="hidden" 
    name="${_csrf.parameterName}"
    value="${_csrf.token}">

csrf防护功能是默认开启的,可以这样关闭

认证用户

我们重写HttpSecurity属性的configure方法后,Spring默认的认证页面没了。

父类的方法中调用了formLogin来提供这个认证表单,我们重写了当然没有了,不过可以加回来。

http.formLogin()
        .and()
        .authorizeRequests()
            .antMatchers("/spitters/me").hasRole("SPITTER")
            .antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
            .anyRequest().permitAll()
        .and()
        .requiresChannel()
            .antMatchers("/spitter/form").requiresSecure();

自定义登录页面

可以使用loginPage方法,指定一个Spring控制器路径,在其中返回相应的逻辑视图名。

需要注意的是,如果不用Thymeleaf和sf:form的话,请记得添加csrf隐藏域。

HttpBasic认证

HttpBasic认证是通过返回http 401状态码通知请求端需要认证,在浏览器中的默认行为是浏览器会弹出一个对话框要求用户认证。

可以通过httpBasic方法启用。

Remember-Me

记住密码Spring也提供了简单的办法。

Spring通过Cookie来存储token来提供RememberMe功能,默认Token存活时间为两周,可以通过tokenValiditySeconds来指定存活的秒数。

token通过用户名,密码,和过期时间以及一个私钥进行散列生成,默认私钥的名字是SpringSecured,可以通过key方法指定,如上图。

嘶可以看到服务器端为我们设置的remember-me字段的cookie

rememberme需要用户在请求表单中指明它需要记住密码,可以在表单中添加如下字段供用户选择。

退出

退出只需要用户请求/logout即可,spring的filter会自动拦截这个请求,做相应的处理。默认情况下,logout后spring会重定向到login?logouturl,可以通过如下方式进行配置。

而且我们可以指定用户退出时过滤器拦截的请求,比如将/logout替换成/signout

保护视图

Spring支持在视图上显示一些认证相关的信息。

可以通过jsp标签库和Thymeleaf的Spring方言。

jsp标签库

安全相关的jsp标签库只有三个标签

使用如下方式声明

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

访问认证信息的细节

我们可以使用authentication标签来显示一些关于认证信息的细节。

比如使用如下方式我们可以渲染出认证的用户名

Hello <security:authentication property="principal.username"/>!

property表示要渲染的属性,principal表示用户认证对象,它其中有关于认证对象的一些基本信息。其他可用的属性如下

条件性渲染

使用authorize可以根据用户被授予的权限条件化渲染页面。

比如下面,当在具有ROLE_SPITTER权限时才渲染发布Spittle的表单

access属性是一个SpEL表达式

Thymeleaf的Spring方言

先引入依赖

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

配置Thymeleaf引擎支持SpringSecurity方言

@Bean
public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver resolver) {
    SpringTemplateEngine templateEngine = new SpringTemplateEngine();
    templateEngine.setTemplateResolver(resolver);
    templateEngine.addDialect(new SpringSecurityDialect());
    return templateEngine;
}

然后我们可以这样使用它

<div sec:authorize="isAuthenticated()">
    Hello <span sec:authentication="name">someone</span>
</div>
posted @ 2021-09-16 21:01  yudoge  阅读(83)  评论(0编辑  收藏  举报