环境准备
需求分析
login.html 页面:不需要登录就可以访问
index.html 页面:登录后才能访问,在该页面中有 业务1,业务2、日志管理 和 用户管理 共4个连接
syslog.html 页面、sysuser.html 页面: 登录后,需要 admin用户才能访问
bizi1.html 页面、biz2.html 页面:登录后,就可以访问
创建父工程
父工程的工程名:spring-security-demo,在父工程中 声明使用 SpringBoot的版本号
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
创建子工程
子工程的工程名:basic-server,主要引入 web 和 thymeleaf 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
创建 application.yml
在该文件中定义端口号
server:
port: 9999
创建页面
页面存放的位置以及名称如图:
html 文件放在 static 和 templates 文件夹下的区别? html 文件放在 static 是不需要经过模板引擎的渲染,即可直接访问到 而放在 templates 下的html是经过模板引擎渲染的,也就是需要 controller 类中的方法返回字符串(视图名)
login页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="/login" method="post">
<span>用户名称</span><input type="text" name="username" /> <br>
<span>用户密码</span><input type="password" name="password" /> <br>
<input type="submit" value="登陆">
</form>
</body>
</html>
index页面
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
</head>
<body>
<br>
<a href="/syslog">日志管理</a>
<br>
<a href="/sysuser">用户管理</a>
<br>
<a href="/biz1">具体业务一</a>
<br>
<a href="/biz2">具体业务二</a>
</body>
</html>
业务1页面
<h1>具体业务一</h1>
业务2页面
<h1>具体业务二</h1>
日志管理页面
<h1>日志管理</h1>
用户管理页面
<h1>用户管理</h1>
main方法
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
controller处理请求
@Controller
public class HelloController {
// 首页
@GetMapping("/index")
public String index() {
return "index";
}
// 日志管理
@GetMapping("/syslog")
public String showOrder() {
return "syslog";
}
// 用户管理
@GetMapping("/sysuser")
public String addOrder() {
return "sysuser";
}
// 具体业务一
@GetMapping("/biz1")
public String updateOrder() {
return "biz1";
}
// 具体业务二
@GetMapping("/biz2")
public String deleteOrder() {
return "biz2";
}
}
测试
访问登录页面:http://localhost:9999/login.html
访问首页:http://localhost:9999/index
HttpBasic模式登录认证
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
编写配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()//开启httpbasic认证
.and()
.authorizeRequests()
.anyRequest()
.authenticated();//所有请求都需要登录认证才能访问
}
}
测试
运行 main 方法,访问页面此时我们会发现,之前能访问的页面都需要输入用户名、密码才能访问了
用户名为:user,密码在控制台打出:
自定义用户名密码
我们可以在 application.yml 文件中定义用户名和密码
spring:
security:
user:
name: admin
password: admin
重新运行 main 方法,然后使用上述定义好的用户名密码试试
formLogin登录认证模式
引入依赖
我们在之前步骤已经引入过了该依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
删除自定义用户名、密码
我们在 HttpBasic 模式登录认证的时候,有在 application.yml 文件中定义用户名、密码。
spring:
security:
user:
name: admin
password: admin
配置类
以下代码是修改后的配置类。主要做的几件事
①、加载用户信息到内存。我们这次使用的是硬编码,加载用户以及用户的角色、权限到内存。
②、往容器中注入密码的加密和校验的接口实现类。
③、定义了登录认证的逻辑(即接收登录参数、登录成功后逻辑)
④、定义了资源访问控制逻辑,即访问哪些资源,需要什么样的权限
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
.formLogin()
.loginPage("/login.html")//一旦用户的请求没有权限就跳转到这个页面
.loginProcessingUrl("/login")//登录表单form中action的地址,也就是处理认证请求的路径
.usernameParameter("username")///登录表单form中用户名输入框input的name名,不修改的话默认是username
.passwordParameter("password")//form中密码输入框input的name名,不修改的话默认是password
.defaultSuccessUrl("/")//登录认证成功后默认转跳的路径
.and()
.authorizeRequests()
.antMatchers("/login.html","/login").permitAll()//不需要通过登录验证就可以被访问的资源路径
.antMatchers("/","/biz1","/biz2") //资源路径匹配
.hasAnyAuthority("ROLE_user","ROLE_admin") //user角色和admin角色都可以访问
.antMatchers("/syslog","/sysuser") //资源路径匹配
.hasAnyRole("admin") //admin角色可以访问
//.antMatchers("/syslog").hasAuthority("sys:log")
//.antMatchers("/sysuser").hasAuthority("sys:user")
.anyRequest().authenticated();
}
@Override
public void configure(WebSecurity web) {
//将项目中静态资源路径开放出来
web.ignoring().antMatchers( "/css/**", "/fonts/**", "/img/**", "/js/**");
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("123456"))
.roles("user")
.and()
.withUser("admin")
.password(passwordEncoder().encode("123456"))
//.authorities("sys:log","sys:user")
.roles("admin")
.and()
.passwordEncoder(passwordEncoder());//配置BCrypt加密
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
测试
重新运行 main 方法,访问其他页面,都会被跳转到登录页面
使用用户名(user/123456)登录,登录到首页,发现业务1、业务2正常访问。而日志管理、用户管理访问报错,如下
使用用户名(admin/123456),登录后可以访问所有资源。
自定义权限访问异常信息处理
当我们访问资源的时候,可能会出现这2种情况。
情况一: 未登录,直接访问资源。默认情况会跳转到登录页面。
情况二:登录系统,访问某些资源没有权限,默认情况会返回 403 的错误页面
解决方法:
情况一:返回 JSON 数据,告知需要先登录才能访问
情况二:返回 JSON 数据,告知权限不足
定义返回消息
public class AjaxResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
public static final String CODE_TAG = "code";
/**
* 返回内容
*/
public static final String MSG_TAG = "msg";
/**
* 数据对象
*/
public static final String DATA_TAG = "data";
/**
* 状态类型
*/
public enum Type {
/**
* 成功
*/
SUCCESS(0),
/**
* 警告
*/
WARN(301),
/**
* 错误
*/
ERROR(500);
private final int value;
Type(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public AjaxResult() {
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param type 状态类型
* @param msg 返回内容
*/
public AjaxResult(Type type, String msg) {
super.put(CODE_TAG, type.value);
super.put(MSG_TAG, msg);
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param type 状态类型
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(Type type, String msg, Object data) {
super.put(CODE_TAG, type.value);
super.put(MSG_TAG, msg);
if (null != data && data != "") {
super.put(DATA_TAG, data);
}
}
/**
* 方便链式调用
*
* @param key 键
* @param value 值
* @return 数据对象
*/
@Override
public AjaxResult put(String key, Object value) {
super.put(key, value);
return this;
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success() {
return AjaxResult.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static AjaxResult success(Object data) {
return AjaxResult.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static AjaxResult success(String msg) {
return AjaxResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static AjaxResult success(String msg, Object data) {
return new AjaxResult(Type.SUCCESS, msg, data);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult warn(String msg) {
return AjaxResult.warn(msg, null);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult warn(String msg, Object data) {
return new AjaxResult(Type.WARN, msg, data);
}
/**
* 返回错误消息
*
* @return
*/
public static AjaxResult error() {
return AjaxResult.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(String msg) {
return AjaxResult.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult error(String msg, Object data) {
return new AjaxResult(Type.ERROR, msg, data);
}
}
引入 fastjson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
定义匿名访问的处理类
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
response.getWriter().print(JSONObject.toJSONString(AjaxResult.error("未登录,无法直接访问资源,请先登陆!")));
}
}
定义没有权限访问的处理类
public class CustomAccessDeineHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
response.getWriter().print(JSONObject.toJSONString(AjaxResult.error("权限不足,无法访问!")));
}
}
修改配置类
在 SpringSecurity的配置类中,添加自定义权限的处理
同账号多端登录踢下线
描述:
例如账号 user,已经登陆了。此时该账号再别处再次登录时会将原来登录的账号踢出下线。
处理策略
此处提供2种策略方案。
第一种是针对 非前后端分离项目的,也就是当该账号再别处登录时,之前登录的账号再刷新后会跳转到登陆页面
第二中是针对 前后端分离项目,当别处登录时,返回 JSON 数据提示用户。
方案一
当账号已在别处登录时,原登录账号再刷新页面时重定向到登录页面
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
//页面跳转的处理逻辑
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
// 是跳转html页面,url代表跳转的地址
redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "/login.html");
}
}
上面策略的代码只是做了页面重定向,如果想要在重定向之前弹窗提示用户,可以这么做:
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
//页面跳转的处理逻辑
//private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
HttpServletResponse response = event.getResponse();
response.setContentType("text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.println("<script>");
out.println("alert('该账号已在别处登录,请重新登录!');");
out.println("location.href='/login.html'");
out.println("</script>");
// 是跳转html页面,url代表跳转的地址
// redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "/login.html");
}
}
方案二
当账号已在别处登录时,原登录账号再刷新页面时 会得到 JSON数据提示
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
//jackson的JSON处理对象
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("code", 403);
map.put("msg", "您的登录已经超时或者已经在另一台机器登录,您被迫下线。"
+ event.getSessionInformation().getLastRequest());
// Map -> Json
String json = objectMapper.writeValueAsString(map);
//输出JSON信息的数据
event.getResponse().setContentType("application/json;charset=UTF-8");
event.getResponse().getWriter().write(json);
}
}
配置策略
测试
重新运行 main 方法, 使用 user/123456 账号分别在 谷歌 和 火狐浏览器上登录,验证是否会别踢出下线!!
退出功能实现
简单实现
在首页添加如下代码:
<a href="/logout" >退出</a>
在 SpringSecurity 的配置类中添加退出配置
@Override
protected void configure(HttpSecurity http) throws Exception {
....省略....
http.logout();
}
测试:
重新运行 main ,使用 user / 123456 登录系统中 index.html 页面,点击页面上 退出按钮后,即将跳转到登录页面
SpringSecurity退出功能默认做了哪些事
- 当前session失效,即:logout的核心需求,session失效就是访问权限的回收。
- 删除当前用户的 remember-me“记住我”功能信息
- clear清除当前的 SecurityContext
- 重定向到登录页面,loginPage配置项指定的页面
定制退出功能
我们可以对退出功能做一些个性化配置,如下:
http.logout()
.logoutUrl("/signout")
.logoutSuccessUrl("/aftersignout.html")
.deleteCookies("JSESSIONID")
如果还嫌不够的话,我们还可以实现 LogoutSuccessHandler 接口,来对我们退出功能进行深度化定制。
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
//这里书写你自己的退出业务逻辑
// 重定向到登录页
response.sendRedirect("/login.html");
}
}
======================================================================================
@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Override
protected void configure(final HttpSecurity http) throws Exception {
http.logout()
.logoutUrl("/signout")
//.logoutSuccessUrl(``"/aftersignout.html"``)
.deleteCookies("JSESSIONID")
//自定义logoutSuccessHandler
.logoutSuccessHandler(myLogoutSuccessHandler);
}
}
记住我功能实现
简单实现
登录表单
在登陆表单中添加记住我的复选框
<label><input type="checkbox" name="remember-me"/>记住密码</label>
配置记住我的功能
在 SpringSecurity的配置文件中,添加记住我的配置 http.rememberMe();
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.rememberMe(); //实现记住我自动登录配置,核心的代码只有这一行
}
}
测试
重新运行 main 方法,先登录,然后重启浏览器,再次直接访问系统资源,发现不需要再次登录。
实现原理
- 当我们登陆的时候,除了用户名、密码,我们还可以勾选remember-me。
- 如果我们勾选了remember-me,当我们登录成功之后服务端会生成一个Cookie返回给浏览器,这个Cookie的名字默认是remember-me;值是一个token令牌。
- 当我们在有效期内再次访问应用时,经过RememberMeAuthenticationFilter,读取Cookie中的token进行验证。验正通过不需要再次登录就可以进行应用访问
个性化设置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.rememberMe()
.rememberMeParameter("remember-me-new")
.rememberMeCookieName("remember-me-cookie")
.tokenValiditySeconds(2 * 24 * 60 * 60);
}
}
设置了http.rememberMe() .rememberMeParameter(“remember-me-new”) 我们需要修改记住我复选框中的name属性值
Token令牌持久化功能
经过上述步骤,我们已经实现了记住我功能,但现在存在一个问题。就是 token 令牌与用户的对应关系 这些数据是存放在内存中的。也就是说当 我们的应用重启后,用户需要重新登录系统。
如何解决?
我们需要将用户和 token令牌的对应关系数据,存放到数据库中进行持久化即可。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
主配置文件
在 application.yml 文件中,添加数据源基本信息
server:
port: 9999
# 数据库连接基本信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/security-demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
创建数据库
创建 security-demo 的数据库,字符编码 utf8mb4
创建表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
修改配置类
在 SpringSecurity的配置类中,我们需要注入 PersistentTokenRepository 类型的bean。同时需要对令牌持久化进行配置
测试
重新运行 main 方法,使用账号密码登录系统,然后重新运行main方法,此时发现在访问系统时不需要重新登录
另外,在数据库表中出现如下数据: