松哥手把手带你入门 Spring Security,别再问密码怎么解密了

因为之前有小伙伴在松哥群里讨论如何给微人事的密码解密,我看到聊天记录后就惊呆了。

无论如何我也得写一篇文章,带大家入门 Spring Security!当我们在一个项目中引入 Spring Security 相关依赖后,默认的就是表单登录,因此我们就从表单登录开始讲起。

本文对应的视频

以下是视频笔记。

1.新建项目

首先新建一个 Spring Boot 项目,创建时引入 Spring Security 依赖和 web 依赖,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ur2YJrgF-1585095356039)(http://img.itboyhub.com//2020/03/spring-security-2-1.png)]

项目创建成功后,Spring Security 的依赖就添加进来了,在 Spring Boot 中我们加入的是 spring-boot-starter-security ,其实主要是这两个:

image

项目创建成功后,我们添加一个测试的 HelloController,内容如下:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

接下来什么事情都不用做,我们直接来启动项目。

在项目启动过程中,我们会看到如下一行日志:

Using generated security password: 30abfb1f-36e1-446a-a79b-f70024f589ab

这就是 Spring Security 为默认用户 user 生成的临时密码,是一个 UUID 字符串。

接下来我们去访问 http://localhost:8080/hello 接口,就可以看到自动重定向到登录页面了:

image

在登录页面,默认的用户名就是 user,默认的登录密码则是项目启动时控制台打印出来的密码,输入用户名密码之后,就登录成功了,登录成功后,我们就可以访问到 /hello 接口了。

在 Spring Security 中,默认的登录页面和登录接口,都是 /login ,只不过一个是 get 请求(登录页面),另一个是 post 请求(登录接口)。

大家可以看到,非常方便,一个依赖就保护了所有接口。

有人说,你怎么知道知道生成的默认密码是一个 UUID 呢?

这个其实很好判断。

和用户相关的自动化配置类在 UserDetailsServiceAutoConfiguration 里边,在该类的 getOrDeducePassword 方法中,我们看到如下一行日志:

if (user.isPasswordGenerated()) {
    logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}

毫无疑问,我们在控制台看到的日志就是从这里打印出来的。打印的条件是 isPasswordGenerated 方法返回 true,即密码是默认生成的。

进而我们发现,user.getPassword 出现在 SecurityProperties 中,在 SecurityProperties 中我们看到如下定义:

/**
 * Default user name.
 */
private String name = "user";
/**
 * Password for the default user name.
 */
private String password = UUID.randomUUID().toString();
private boolean passwordGenerated = true;

可以看到,默认的用户名就是 user,默认的密码则是 UUID,而默认情况下,passwordGenerated 也为 true。

2.用户配置

默认的密码有一个问题就是每次重启项目都会变,这很不方便。

在正式介绍数据库连接之前,松哥先和大家介绍两种非主流的用户名/密码配置方案。

2.1 配置文件

我们可以在 application.properties 中配置默认的用户名密码。

怎么配置呢?大家还记得上一小节我们说的 SecurityProperties,默认的用户就定义在它里边,是一个静态内部类,我们如果要定义自己的用户名密码,必然是要去覆盖默认配置,我们先来看下 SecurityProperties 的定义:

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {

这就很清晰了,我们只需要以 spring.security.user 为前缀,去定义用户名密码即可:

spring.security.user.name=javaboy
spring.security.user.password=123

这就是我们新定义的用户名密码。

在 properties 中定义的用户名密码最终是通过 set 方法注入到属性中去的,这里我们顺便来看下 SecurityProperties.User#setPassword 方法:

public void setPassword(String password) {
    if (!StringUtils.hasLength(password)) {
        return;
    }
    this.passwordGenerated = false;
    this.password = password;
}

从这里我们可以看到,application.properties 中定义的密码在注入进来之后,还顺便设置了 passwordGenerated 属性为 false,这个属性设置为 false 之后,控制台就不会打印默认的密码了。

此时重启项目,就可以使用自己定义的用户名/密码登录了。

2.2 配置类

除了上面的配置文件这种方式之外,我们也可以在配置类中配置用户名/密码。

在配置类中配置,我们就要指定 PasswordEncoder 了,这是一个非常关键的东西。

考虑到有的小伙伴对于 PasswordEncoder 还不太熟悉,因此,我这里先稍微给大家介绍一下 PasswordEncoder 到底是干嘛用的。要说 PasswordEncoder ,就得先说密码加密。

2.2.1 为什么要加密

2011 年 12 月 21 日,有人在网络上公开了一个包含 600 万个 CSDN 用户资料的数据库,数据全部为明文储存,包含用户名、密码以及注册邮箱。事件发生后 CSDN 在微博、官方网站等渠道发出了声明,解释说此数据库系 2009 年备份所用,因不明原因泄露,已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最触目惊心的莫过于 CSDN 把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄露就会造成很大的安全隐患。由于有了这么多前车之鉴,我们现在做系统时,密码都要加密处理。

这次泄密,也留下了一些有趣的事情,特别是对于广大程序员设置密码这一项。人们从 CSDN 泄密的文件中,发现了一些好玩的密码,例如如下这些:

  • ppnn13%dkstFeb.1st 这段密码的中文解析是:娉娉袅袅十三余,豆蔻梢头二月初。
  • csbt34.ydhl12s 这段密码的中文解析是:池上碧苔三四点,叶底黄鹂一两声
  • ...

等等不一而足,你会发现很多程序员的人文素养还是非常高的,让人啧啧称奇。

2.2.2 加密方案

密码加密我们一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。我们常用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)。

但是仅仅使用散列函数还不够,为了增加密码的安全性,一般在密码加密过程中还需要加盐,所谓的盐可以是一个随机数也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码密文也不相同,这可以极大的提高密码的安全性。但是传统的加盐方式需要在数据库中有专门的字段来记录盐值,这个字段可能是用户名字段(因为用户名唯一),也可能是一个专门记录盐值的字段,这样的配置比较繁琐。

Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。

不同于 Shiro 中需要自己处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。

而 BCryptPasswordEncoder 就是 PasswordEncoder 接口的实现类。

2.2.3 PasswordEncoder

PasswordEncoder 这个接口中就定义了三个方法:

public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}
  1. encode 方法用来对明文密码进行加密,返回加密之后的密文。
  2. matches 方法是一个密码校对方法,在用户登录的时候,将用户传来的明文密码和数据库中保存的密文密码作为参数,传入到这个方法中去,根据返回的 Boolean 值判断用户密码是否输入正确。
  3. upgradeEncoding 是否还要进行再次加密,这个一般来说就不用了。

通过下图我们可以看到 PasswordEncoder 的实现类:

image

2.2.4 配置

预备知识讲完后,接下来我们来看具体如何配置:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("javaboy.org")
                .password("123").roles("admin");
    }
}
  1. 首先我们自定义 SecurityConfig 继承自 WebSecurityConfigurerAdapter,重写里边的 configure 方法。
  2. 首先我们提供了一个 PasswordEncoder 的实例,因为目前的案例还比较简单,因此我暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例即可。
  3. configure 方法中,我们通过 inMemoryAuthentication 来开启在内存中定义用户,withUser 中是用户名,password 中则是用户密码,roles 中是用户角色。
  4. 如果需要配置多个用户,用 and 相连。

为什么用 and 相连呢?

在没有 Spring Boot 的时候,我们都是 SSM 中使用 Spring Security,这种时候都是在 XML 文件中配置 Spring Security,既然是 XML 文件,标签就有开始有结束,现在的 and 符号相当于就是 XML 标签的结束符,表示结束当前标签,这是个时候上下文会回到 inMemoryAuthentication 方法中,然后开启新用户的配置。

配置完成后,再次启动项目,Java 代码中的配置会覆盖掉 XML 文件中的配置,此时再去访问 /hello 接口,就会发现只有 Java 代码中的用户名/密码才能访问成功。

3.自定义表单登录页

默认的表单登录有点丑(实际上现在默认的表单登录比以前的好多了,以前的更丑)。

但是很多时候我们依然绝对这个登录页面有点丑,那我们可以自定义一个登录页面。

一起来看下。

3.1 服务端定义

然后接下来我们继续完善前面的 SecurityConfig 类,继续重写它的 configure(WebSecurity web)configure(HttpSecurity http) 方法,如下:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login.html")
            .permitAll()
            .and()
            .csrf().disable();
}
  1. web.ignoring() 用来配置忽略掉的 URL 地址,一般对于静态文件,我们可以采用此操作。
  2. 如果我们使用 XML 来配置 Spring Security ,里边会有一个重要的标签 <http>,HttpSecurity 提供的配置方法 都对应了该标签。
  3. authorizeRequests 对应了 <intercept-url>
  4. formLogin 对应了 <formlogin>
  5. and 方法表示结束当前标签,上下文回到HttpSecurity,开启新一轮的配置。
  6. permitAll 表示登录相关的页面/接口不要被拦截。
  7. 最后记得关闭 csrf ,关于 csrf 问题我到后面专门和大家说。

当我们定义了登录页面为 /login.html 的时候,Spring Security 也会帮我们自动注册一个 /login.html 的接口,这个接口是 POST 请求,用来处理登录逻辑。

3.2 前端定义

松哥这里准备了一个还过得去的登录页面,如下:

image

我们将登录页面的相关静态文件拷贝到 Spring Boot 项目的 resources/static 目录下:

image

前端页面比较长,这里我把核心部分列出来(完整代码我会上传到 GitHub:https://github.com/lenve/spring-security-samples):

<form action="/login.html" method="post">
    <div class="input">
        <label for="name">用户名</label>
        <input type="text" name="username" id="name">
        <span class="spin"></span>
    </div>
    <div class="input">
        <label for="pass">密码</label>
        <input type="password" name="password" id="pass">
        <span class="spin"></span>
    </div>
    <div class="button login">
        <button type="submit">
            <span>登录</span>
            <i class="fa fa-check"></i>
        </button>
    </div>
</form>

form 表单中,注意 action 为 /login.html ,其他的都是常规操作,我就不重复介绍了。

好了,配置完成后,再去重启项目,此时访问任意页面,就会自动重定向到我们定义的这个页面上来,输入用户名密码就可以重新登录了。

4.小节

这篇文章和大家简单聊一下 Spring Security 入门,表单配置还有很多细节,下篇文章我们继续。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容