1.spring security简介:
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements
2.核心功能
作为一个权限管理框架,其最核心的功能:
<li> 认证 </li>
<li> 授权 </li>
3.项目部署(IDEA)
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.1用户配置
3.11配置文件
我们可以在 application.properties 中配置默认的用户名密码:
spring.security.user.name=javaboy
spring.security.user.password=123
3.12配置类
基本配置
@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");
}
}
- 首先我们自定义 SecurityConfig 继承自 WebSecurityConfigurerAdapter,重写里边的 configure 方法。
- 首先我们提供了一个 PasswordEncoder 的实例,因为目前的案例还比较简单,因此我暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例即可。
- configure 方法中,我们通过 inMemoryAuthentication 来开启在内存中定义用户,withUser 中是用户名,password 中则是用户密码,roles 中是用户角色。如果需要配置多个用户,用 and 相连
原始表单登录页
很丑是不是,下面是怎样自行配置form表单代码实现:
@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")//放置于static/目录下
.permitAll()
.and()
.csrf().disable();
}
- web.ignoring() 用来配置忽略掉的 URL 地址,一般对于静态文件,我们可以采用此操作。
- 如果我们使用 XML 来配置 Spring Security ,里边会有一个重要的标签 <http>,HttpSecurity 提供的配置方法 都对应了该标签。
- authorizeRequests 对应了
<intercept-url>
。 - formLogin 对应了
<formlogin>
。 - and 方法表示结束当前标签,上下文回到HttpSecurity,开启新一轮的配置。
- permitAll 表示登录相关的页面/接口不要被拦截。
- 最后记得关闭 csrf ,关于 csrf 问题我到后面专门和大家说。
当我们定义了登录页面为 /login.html 的时候,Spring Security 也会帮我们自动注册一个 /login.html 的接口,这个接口是 POST 请求,用来处理登录逻辑。
3.2 登录form表单
3.2.1原始登录地址:/login如何改变
当我们定义了登录页面为 /login.html 的时候,Spring Security 也会帮我们自动注册一个 /login.html 的接口,这个接口是 POST 请求,用来处理登录逻辑。
GET http://localhost:8080/login
POST http://localhost:8080/login
如果是 GET 请求表示你想访问登录页面,如果是 POST 请求,表示你想提交登录数据。
登录界面与登录接口分离
- 在 SecurityConfig 中,我们可以通过 loginProcessingUrl 方法来指定登录接口地址,如下:
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
- 在登录界面的form表单action属性修改:
<form action="/doLogin" method="post">
<!--省略-->
</form>
3.22 登录参数
form表单中的用户名以及密码参数默认是不可变的
<input type="text" name="username" id="name">
<input type="password" name="password" id="pass">
那么如何改变呢???
1.在 SecurityConfig 中配置参数名
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("name")
.passwordParameter("passwd")
.permitAll()
.and()
2.配置前端页面
<input type="text" name="name" id="name">
<input type="password" name="passwd" id="pass">
3.3登录回调
3.31成功回调
方法
在 Spring Security 中,和登录成功重定向 URL 相关的方法有两个:
1.defaultSuccessUrl
2.successForwardUrl
(注意两者只能使用一个)
区别
- defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到 /index,如果你是在浏览器中输入了其他地址,例如 http://localhost:8080/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /index ,而是来到 /hello 页面。
- defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为 false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和 successForwardUrl 一致。
- successForwardUrl 表示不管你是从哪里来的,登录后一律跳转到 successForwardUrl 指定的地址。例如 successForwardUrl 指定的地址为 /index ,你在浏览器地址栏输入 http://localhost:8080/hello,结果因为没有登录,重定向到登录页面,当你登录成功之后,就会服务端跳转到 /index 页面;或者你直接就在浏览器输入了登录页面地址,登录成功后也是来到 /index。
SecurityConfig配置
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("name")
.passwordParameter("passwd")
.defaultSuccessUrl("/index")
.successForwardUrl("/index")
.permitAll()
.and()
3.32失败回调
#### 方法
- failureForwardUrl
- failureUrl
#### 区别
ailureForwardUrl 是登录失败之后会发生服务端跳转,failureUrl 则在登录失败之后,会发生重定向。
#### SecurityConfig配置
.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))
.logoutSuccessUrl("/index")
.deleteCookies()
.clearAuthentication(true)
.invalidateHttpSession(true)
.permitAll()
.and()
- 默认注销的 URL 是 /logout,是一个 GET 请求,我们可以通过 logoutUrl 方法来修改默认的注销 URL。
- logoutRequestMatcher 方法不仅可以修改注销 URL,还可以修改请求方式,实际项目中,这个方法和 logoutUrl 任意设置一个即可。
- logoutSuccessUrl 表示注销成功后要跳转的页面。
- deleteCookies 用来清除 cookie。
- clearAuthentication 和 invalidateHttpSession 分别表示清除认证信息和使 HttpSession 失效,默认可以不用配置,默认就会清除。
4. 授权
4.1何为授权
所谓的授权,就是用户如果要访问某一个资源,我们要去检查用户是否具备这样的权限,如果具备就允许访问,如果不具备,则不允许访问。
4.2demo
4.2.1 测试用户
根据前面文章中SecurityConfig配置的用户信息进行用户测试
第二种配置方式参考链接:江南一点雨
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javaboy")
.password("123")
.roles("admin")//权限管理员
.and()
.withUser("sang")
.password("123")
.roles("user");
}
4.22测试接口(Controller)
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "admin";
}
@GetMapping("/user/hello")
public String user() {
return "user";
}
}
- /hello 是任何人都可以访问的接口
- /admin/hello 是具有 admin 身份的人才能访问的接口
- /user/hello 是具有 user 身份的人才能访问的接口
- 所有 user 能够访问的资源,admin 都能够访问
4.23 配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and();
}
** 匹配多层路径
* 匹配一层路径
? 匹配任意单个字符`
注意代码中配置的三条规则的顺序非常重要,Spring Security 在匹配的时候是按照从上往下的顺序来匹配,一旦匹配到了就不继续匹配了,所以拦截规则的顺序不能写错。
4.24权限继承
比若说user可以访问的资源怎样使admin也能访问到呢?(这里有一个误区就是会误以为admin角色能够访问user角色的资源,其实没有角色授权时是办不到的)这种情景就用到了角色继承。
4.241方法
SecurityConfig 中添加如下代码来配置角色继承关系即可:
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}
注意,在配置时,需要给角色手动加上 ROLE_ 前缀。上面的配置表示 ROLE_admin 自动具备 ROLE_user 的权限。
4.25 demo代码链接:gitee
5. 将用户数据存储到数据库
此处原文链接:Spring Security 如何将用户数据存入数据库? 此处本人在写博客时没有用到,在项目中用到时在详细写一下。
6. Spring Security 实现自动登录功能
何为自动登录就不用说了吧,就是登录跳过登录界面:
6.1 实现:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.and()
.csrf().disable();
}
没错,就是一个rememberMe()函数
这样的话,即使你关闭了已经认证登陆的网页,再次打开后就不用再次登录了。
7. 实现单用户登录
类似qq中第二个账号登录以后第一个账号会被迫掉线。
7.1 踢掉已经登录的用户
代码实现
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable()
.sessionManagement()
.maximumSessions(1);
}
此处代码实现的就是最大登录用户数为1
分别在谷歌以及火狐登录,第一次登录的网站再次访问资源时会出现以下界面内容
7.2 禁止新用户登录
代码实现
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true);//关键代码
}
不过还没完,我们还需要再提供一个 Bean:
1
2
3
4
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
为什么要加这个 Bean 呢?因为在 Spring Security 中,它是通过监听 session 的销毁事件,来及时的清理 session 的记录。用户从不同的浏览器登录后,都会有对应的 session,当用户注销登录之后,session 就会失效,但是默认的失效是通过调用 StandardSession#invalidate 方法来实现的,这一个失效事件无法被 Spring 容器感知到,进而导致当用户注销登录之后,Spring Security 没有及时清理会话信息表,以为用户还在线,进而导致用户无法重新登录进来(小伙伴们可以自行尝试不添加上面的 Bean,然后让用户注销登录之后再重新登录)。
为了解决这一问题,我们提供一个 HttpSessionEventPublisher ,这个类实现了 HttpSessionListener 接口,在该 Bean 中,可以将 session 创建以及销毁的事件及时感知到,并且调用 Spring 中的事件机制将相关的创建和销毁事件发布出去,进而被 Spring Security 感知到,该类部分源码如下:
public void sessionCreated(HttpSessionEvent event) {
HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession());
getContext(event.getSession().getServletContext()).publishEvent(e);
}
public void sessionDestroyed(HttpSessionEvent event) {
HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());
getContext(event.getSession().getServletContext()).publishEvent(e);
}
OK,虽然多了一个配置,但是依然很简单!
8. Spring Boot中的密码加密
8.1加密方案
密码加密我们一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法。
散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。
我们常用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)。
但是,单纯使用散列函数也存在着诟病,比如两个密码的明文相同的话,那么加密以后的密码也是相同的,因此,在这里我们就需要在密码加密的同时进行加盐处理。
8.11 何为盐?
所谓的盐可以是一个随机数也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码密文也不相同,这可以极大的提高密码的安全性。
在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。
8.2实践
在 Spring Security 中提供了 BCryptPasswordEncoder,使得密码加密加盐变得非常容易。只需要提供 BCryptPasswordEncoder 这个 Bean 的实例即可
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
创建 BCryptPasswordEncoder 时传入的参数 10 就是 strength,即密钥的迭代次数(也可以不配置,默认为 10)。同时,配置的内存用户的密码也不再是 123 了,如下:
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
.roles("ADMIN", "USER")
.and()
.withUser("sang")
.password("$2a$10$eUHbAOMq4bpxTvOVz33LIehLe3fu6NwqC9tdOcxJXEhyZ4simqXTC")
.roles("USER");
这里的密码就是使用 BCryptPasswordEncoder 加密后的密码,虽然 admin 和 sang 加密后的密码不一样,但是明文都是 123。配置完成后,使用 admin/123 或者 sang/123 就可以实现登录.
在前端注册或者其余表单等写入要加密的数据后,在后端获取到并进行加密处理如下:
@Service
public class RegService {
public int reg(String username, String password) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
String encodePasswod = encoder.encode(password);
return saveToDb(username, encodePasswod);
}
}
此时 encodePasswod存储的就是加密后的数据
关于codec加密方式有兴趣的小伙伴可以参考链接Spring Boot 中密码加密的两种姿势!