SpringSecurity(基于狂神说Java)
1、简介
Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目用Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
-
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户;
-
授权:经过认证后判断当前用户是否有权限进行某个操作;
-
而认证和授权也是SpringSecurity作为安全框架的核心功能。
以前纯Javaweb程序的用户验证使用了拦截器,过滤器我们需要写很多原生的代码,很雍余,所以Spring Security 来精简它了。
2、快速上手
2.1、新建项目SpringSecurity(老版本2.2.1的)
新建项目选择web的依赖和模板引擎以及security依赖
将静态资源导入
把thymeleaf的缓存关掉
spring.thymeleaf.cache=false
然后编写路由controller,先让几个页面可以访问到
package com.zm.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class RouterController {
@RequestMapping({"/","/index","/index.html"})
public String index(){
return "index";
}
@RequestMapping("/toLogin")
public String login(){
return "views/login";
}
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") String id){
return "views/level1/"+id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") String id){
return "views/level2/"+id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") String id){
return "views/level3/"+id;
}
}
3、授权
有几个类需要记住:
- WebSecurityConfigurerAdapter(现已经弃用):自定义Security策略,自定义的配置类就继承它,然后重写方法即可。本测试使用的是springboot2.2.1版本的,对应的SpringSecurity也是一样
- AuthenticationManagerBuilder:自定义认证策略
- @EnableWebSecurity:开启WebSecurity模式
SpringSecurity的两个主要目标是“认证”(Authentication)与“授权”(Authorization)
新建config目录,新建SecurityConfig配置类继承WebSecurityConfigurerAdapter,然后开启@EnableWebSecurity。
配置SecurityConfig,这里设置首页的三个模块都必须有权限才能访问,首页和登录页面谁都可以访问
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//链式编程
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置首页所有人都可以访问,但功能页只有对应权限的人才能访问
//请求授权
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//没有权限就默认到登录页面,开启登录的页面
http.formLogin();
}
}
运行测试,发现首页正常打开,但是一点下面的任意一个模块就跳到SpringSecurity的默认登录页了,启动时会给你密码,用户名是user。
首页:
点level-1-1模块,它让登录,输入用户名user密码复制后台生成的
登录后发现它报错403权限不够。
4、认证
我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法。
//自定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这里只是在内存中定义的,还可以去数据库拿来
//这里的密码都使用{noop}代表明文,不行就加密,不然会报500错误密码编码有误
/* 加密写法:
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("张三").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
* */
auth.inMemoryAuthentication()
.withUser("张三").password("{noop}123456").roles("vip2","vip3")
.and().withUser("root").password("{noop}123456").roles("vip1","vip2","vip3")
.withUser("游客").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
经过测试发现登录张三的号时,模块2和模块3都能访问,而1不行。
开启注销功能
只需要在写登录的地方再写一个注销就好了
//注销后退到首页
http.logout().logoutSuccessUrl("/");
去前端增加一个注销,这里的/logout请求不是自己写的controller,而是SpringSecurity自带的,它有提示。
重启测试
他还提示你是否注销
注销之后又重新回到了首页,但再点下面的模块就又让你登录了。
5、SpringSecuity整合Thymeleaf
首页的登录和注销两个模块不应该同时显示,如果用户登陆了就显示注销并且显示当前用户名,如果未登录就显示登录,所以需要整合Thymeleaf和springsecurity搭配。
添加依赖,如果当时创建项目的时候就选了SpringSecuity和Thymeleaf,idea会自动导入的。
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
修改首页,添加内容
<!--未登录-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
<!--登录了就显示登录的用户名-->
<div sec:authorize="isAuthenticated()">
<a class="item">
用户名:<span sec:authentication="principal.username"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
<a class="item" th:href="@{/logout}">
<i class="address card icon"></i> 注销
</a>
</div>
没登录时上面就一个登录
登录之后有用户名和角色还有注销,现在登录一个张三,可以正常显示出来。
现实场景:不同的用户对每个模块有不同的权限,之前都是张三只能让它访问到模块2和3模块1不能访问,现在让它不能访问的模块不显示出来,这样就比较直接,不同用户直接显示自己对应的能访问的模块。
张三只能看到模块2和3,root可以看到全部,游客登录只能看到模块1。
需要到首页添加功能,hasRole能指定角色(也就是权限),用户符合模块的权限就给显示,不然不显示。
<div class="column" sec:authorize="hasRole('vip1')">
模块1部分
</div>
<div class="column" sec:authorize="hasRole('vip2')">
模块2部分
</div>
<div class="column" sec:authorize="hasRole('vip3')">
模块3部分
</div>
进入首页一个模块都没有,因为没登陆。
先登录游客,就只显示了一个模块1,然后注销。
登录张三,只显示模块2和3,然后注销。
现在登录root,拥有全部权限
6、自定义登录页和记住我
6.1、记住我
最直接就在配置类里加上http.rememberMe()方法,对,就这么直接。
//记住我功能
http.rememberMe();
现在登录的话就有一个选择,记住我在本电脑上
关闭这个标签页再访问首页直接就是张三登录后的页面了。
在选择了记住我之后登录,就会生成一个jsessionID和remember-me存在 了cookie里,这样只要服务没关,关闭浏览器再打开页面还是原样。
6.2、自定义登录页
需要在配置类中的登录部分加上loginPage指定自己的登录页就好,由于之前写的有进入登录页的controller所以直接写请求/tolLogin,自定义的登录页点登录之后提交的表单还需要有人处理,所以使用loginProcessingUrl()方法就行指定处理登录请求的URL,登录提交上来的表单还要指定接收登录的用户名和密码的参数才能处理,使用usernameParameter()和passwordParameter()方法指定从前端接收的用户名和密码就行验证。
// loginProcessingUrl()方法:指定处理登录请求的URL
http.formLogin().loginPage("/toLogin").usernameParameter("username").passwordParameter("password").loginProcessingUrl("/login");
前端的登录页提交的表单,提交到/login
<form th:action="@{/login}" method="post">
再加上记住我功能,在登录页加一个多选框
<input type="checkbox" name="remember-me"> 记住我
然后后端再定制记住我的参数
http.rememberMe().rememberMeParameter("remember-me");
点击首页的登录直接就跳到了我们自己的登录页
正常登录
关闭浏览器再打开页面,root的页面也还在,说明记住我可用。
SecurityConfig配置类:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//链式编程
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置首页所有人都可以访问,但功能页只有对应权限的人才能访问
//请求授权
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//没有权限就默认到登录页面,开启登录的页面
// loginProcessingUrl()方法:指定处理登录请求的URL
http.formLogin().loginPage("/toLogin").usernameParameter("username").passwordParameter("password").loginProcessingUrl("/login");
//注销功能
http.logout().logoutSuccessUrl("/");
//记住我功能
http.rememberMe().rememberMeParameter("remember-me");
}
//自定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这里只是在内存中定义的,还可以去数据库拿来
//这里的密码都使用{noop}代表明文,不行就加密,不然会报500错误密码编码有误
//加密写法:
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("张三").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("游客").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}