SpringInAction 第四章笔记 保护Spring

项目地址:https://github.com/AganRun/SpringInAction/tree/master/Chapter4/taco-security

第四章 配置Spring Security

初次使用

当加入了starter启动器后,项目启动时,在日志中会出现一个账号及随机密码

Using generated security password: cf18a7e9-ffa6-429f-9331-40d2dd121f53

此时登录项目会有一个登录页面,输入账号密码才能访问

security starter的影响

  • 所有HTTP请求都需要认证,认证过程是通过HTTP basic认证对话框实现的
  • 没有特定的角色及权限,只有一个user用户
  • 没有登录页面

配置Security

只有一个用户显然满足不了需求,security有四种配置用户的方式

  • 基于内存的用户存储 (将账号密码直接硬编码到配置代码中,优点:方便快捷,可以用来调试。缺点:项目上线后不方便修改用户)
  • 基于JDBC的用户存储
  • 以LDAP作为后端的用户存储
  • 自定义用户详情服务

JDBC配置Security

当配置了数据源之后,Spring有一套默认的用户搜索SQL。若表于其不匹配,则可以自定义SQL去配置用户。

酱默认的SQL查询替换为自定义的设计时,很重要的一点是遵循查询的基本协议。所有查询将用户名作为唯一参数

@Configuration
@EnableWebSecurity
public class SecurityJdbcConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    DataSource dataSource;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username, password, enabled from Users where username = ?"
                )
                .authoritiesByUsernameQuery(
                            "select username, authority from UserAuthorities where username = ?"
                );
    }
}

密码加密

数据库明文存储密码是很危险的,passwordEncoder()方法可以接受任意的PasswordEncoder接口的实现

  • BCryptPasswordEncoder: 使用bcrypt强哈希加密
  • NoOpPasswordEncoder: 不进行任何转码(已过期)
  • Pbkdf2PasswordEncoder: 使用PBKDF2加密
  • SCryptPasswordEncoder: 使用scrypt哈希加密
  • StandardPasswordEncoder: 使用SHA-256哈希加密

逻辑是:数据库中的密码应该永远不会被解密。将输入的密码进行算法转码,然后与库中的密码对比。

@Bean
public static PasswordEncoder passwordEncoder() {
    //自定义一个加密流程
    return new PasswordEncoder() {
        @Override
        public String encode(CharSequence charSequence) {
            //加1加密,哈哈
            System.out.println("encode" + charSequence);
            return charSequence.toString() + 1;
        }

        @Override
        public boolean matches(CharSequence charSequence, String s) {
            System.out.println("match,charSequence:" + charSequence + ", dbPassword:" + s);
            return encode(charSequence).equals(s);
        }
    };
}


DB: user1/12341
输入: user1/1234

控制台输出
match,charSequence:1234, dbPassword12341
encode1234

自定义用户认证

  1. 自定义一个实体类,例如User,去实现UserDetails接口
  2. 定义对应的Repository,Service(需要实现UserDetailsService的loadUserByUsername(String username))
  3. 配置类中,auth.userDetailsService(注入的service)

保护Web请求

在WebSecurityConfigureAdapter的Configure(HttpSecurity http)方法中
可以配置的功能有:

  • 为某个请求提供服务之前,先预先满足条件
  • 配置自定义的登录页
  • 支持用户退出应用
  • 预防跨站请求伪造

保护请求 & 登录

可以通过配置路径与对应的角色控制,并指定登录页

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/design", "/orders")
//                    .hasRole("USER")
                .access("hasRole('USER')")     // design 和 orders 路径,需要有user角色
            .antMatchers("/", "/**")
//                    .permitAll();
                .access("permitAll")    //对于/和/**路径不拦截
        .and()
            .formLogin()
                .loginPage("/login")   //登录页面
                .defaultSuccessUrl("/design")  //成功后重定向到design页面
        ;
}

存在很多的配置方法,列举其中的几个

方法 能做什么
access(String) 如果给定的SpEL表达式计算结果为TRUE,就允许访问
authenticated() 允许认证过的用户访问
denyAll() 无条件拒绝所有访问
hasIpAddress(String) 如果请求来自给定的IP地址,允许访问
hasRole(String) 如果用户具备指定的角色,允许访问
not() 对其他方法的结果求反
permitAll() 无条件允许访问
rememberMe() 如果用户通过Remember-me认证的,允许访问

SpEl表达式可以编写复杂的逻辑。例如只允许具备ROLE_USER权限的用户,在星期二下午创建新的Taco

退出

.and().logout().logoutSuccessUrl("/");

POST请求403?

在上述配置完之后,即使用户已经登录了,在提交订单,请求/design时依旧会被拦截,页面403。
那是因为SpringSecurity默认是开启了内置的CSRF保护, 建议不要关掉。先了解一下什么是CSRF。

CSRF

跨站请求伪造(Cross-Site Request Forgery, CSRF)。
它会让用户在一个恶意Web页面填写信息,然后自动将表单以攻击受害者的身份提交到另外一个应用上。

可以理解为:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

预防措施 :应用在展现表单的时候,生成一个CSRF token,放在隐藏域存储起来,在提交表单的时候将token一起发送至服务器,由服务器对比匹配。

可以加入隐藏域(JSP、Thymeleaf默认生成)

<input type="hidden" name="_csrf" th:value="${_csrf.token}" />

提交表单时,使用th:action属性

<form method="POST" th:action="@{/login}" id="loginForm" >

了解用户是谁

如何获取当前用户的信息

  • 注入Principal对象到控制器
  • 注入Authentication对象到控制器
  • 使用@AuthenticationPricipal注解标注方法
  • 使用SecurityContextHolder来获取安全上下文
public String processDesign(@Valid Taco taco, Errors errors, @ModelAttribute Order order, Principal principal) {
    String username = principal.getName()   //获取用户的username=>获取用户信息  缺点:业务中会掺杂安全代码

public String processDesign(@Valid Taco taco, Errors errors, @ModelAttribute Order order, Authentication authentication) {
    User user = (User) authentication.getPrincipal();       //强转User对象

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    User user = (User) authentication.getPrincipal();       //繁琐,但是任何地方都可以,不限制在Controller中

public String processDesign(@Valid Taco taco, Errors errors, @ModelAttribute Order order, @AuthenticationPrincipal User user) {
    //使用注解,最方便
}
posted @ 2020-06-17 22:50  浮梦  阅读(223)  评论(0编辑  收藏  举报