使用SpringSecurity框架的基础操作
一、SpringSecurity框架用法简介
用户登录系统时我们需要协助 SpringSecurity 把用户对应的角色、权限组装好,同时把各个
资源所要求的权限信息设定好,剩下的“登录验证”、“权限验证”等等工作都交给 SpringSecurity。
二、权限管理过程中的相关概念
1、主体
英文单词:principal
使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。
2、认证
英文单词:authentication
权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。
笼统的认为就是以前所做的登录操作。
3、授权
英文单词:authorization
将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。
所以简单来说,授权就是给用户分配权限。
三、权限管理的主流框架
1、SpringSecurity
Spring 技术栈的组成部分。
通过提供完整可扩展的认证和授权支持保护你的应用程序。
SpringSecurity 特点:
-
和 Spring 无缝整合。
-
全面的权限控制。
-
专门为 Web 开发而设计。
- 旧版本不能脱离 Web 环境使用。
- 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独 引入核心模块就可以脱离 Web 环境。
-
重量级。
2、Shiro
Apache 旗下的轻量级权限控制框架。
特点:
-
轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
-
通用性。
- 好处:不局限于 Web 环境,可以脱离 Web 环境使用。
- 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。
四、在web项目的基础上搭建SpringSecurity环境
① 在pom.xml中导入SpringSecurity 依赖
<!-- SpringSecurity 对 Web 应用进行权限管理 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
<!-- SpringSecurity 配置 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
<!-- SpringSecurity 标签库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
② 在web.xml中加入 SpringSecurity 控制权限的 Filter
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
③ 加入配置类 WebAppSecurityConfig.class
/**
* @author: Herz
* @date: 2021/7/19 12:43
*/
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
最后运行效果(这是默认行为的SpringSecurity)
-
所有请求都被 SpringSecurity 拦截,要求登录才可以访问。
-
静态资源也都被拦截,要求登录。
-
登录失败有错误提示。
五、SpringSecurity一些基本的设置
1、实现登录并访问具体资源
1.1、基于内存登录
① 需要在SpringSecurity的配置类WebAppSecurityConfig.class中重写父类方法 configure(AuthenticationManagerBuilder builder)
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder
.inMemoryAuthentication() // 在内存中完成账号、密码的检查
.withUser("tom") // 指定账号
.password("123456") // 指定密码
.roles("ADMIN") // 指定角色
.and()
.withUser("jerry") // 指定账号
.password("123123") // 指定密码
.authorities("UPDATE") // 指定权限
;
}
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp 路径授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui/** 路径授权
.permitAll() // 可以无条件访问
.and()
.authorizeRequests() // 对请求进行授权
.anyRequest() // 任意请求
.authenticated() // 需要登录后才可以访问
.and()
.formLogin() // 使用表单形式登录
// 关于 loginPage() 方法的特殊说明
// 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
// /index.jsp GET - 去登录页
// /index.jsp POST - 提交登录的表单
// /index.jsp?error GET - 登录失败
// /index.jsp?logout GET - 退出登录
.loginPage("/index.jsp") // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)
// loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
.loginProcessingUrl("/do/login.html") // 指定提交表单的地址
.usernameParameter("loginAcct") // 指定登录账号的请求参数名
.passwordParameter("userPwd") // 指定登录密码的请求参数名
.defaultSuccessUrl("/main.html") // 指定登录成功后前往的地址
;
}
}
② 在jsp页面中需要加入
<p>${SPRING_SECURITY_LAST_EXCEPTION.message}</p>
<form action="${pageContext.request.contextPath }/do/login.html" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
……………………中间省略代码
</form>
补:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
这个在该项目中由于方便,已经禁用掉了,具体代码请关注下面代码中。
这里的 _csrf 是防止跨站请求伪造:Cross-site request forgery
即发送登录请求时没有携带_csrf 值,会报错:
最后的实现效果:登录成功后具体资源都可以访问了。
1.2、基于数据库的登录
① 编写一个类 SecurityAdmin
用来封装 Admin对象 和 角色、权限 信息。
/**
*
* 用来 封装 Admin对象 和 角色、权限 信息 的类
*
* 考虑到 User 对象中 仅仅包含了 账号 和 密码 ,
* 为了能够获取到原始的 Admin 对象,专门创建这个类对 User 类进行扩展
*
*
* @author: Herz
* @date: 2021/7/21 14:49
*/
public class SecurityAdmin extends User {
// 原始的Admin对象,包含Admin的全部属性
private Admin originalAdmin;
public SecurityAdmin(
// 传入的原始Admin对象
Admin originalAdmin,
// 创建角色、权限信息的集合
List<GrantedAuthority> authorities) {
// 调用父类构造器
super(originalAdmin.getUserName(),originalAdmin.getUserPwd(),authorities);
// 给本类的Admin对象赋值
this.originalAdmin = originalAdmin;
// 将原始Admin对象中得密码擦除
this.originalAdmin.setUserPwd(null);
}
// 对外提供的获取原始Admin对象的get()方法
public Admin getOriginalAdmin() {
return originalAdmin;
}
}
② 自定义数据库查询方式
/**
*
* 自定义SpringSecurity数据库查询方式
* UserDetailsService 实现类
*
* @author: Herz
* @date: 2021/7/21 15:09
*/
@Component
public class CrowdUserDetailsService implements UserDetailsService {
@Autowired
AdminService adminService;
@Autowired
RoleService roleService;
@Autowired
AuthService authService;
@Override
public UserDetails loadUserByUsername(
// 表单提交的用户名
String userName) throws UsernameNotFoundException {
// 1、根据 表单 提交的用户名 查询Admin对象
Admin admin = adminService.getAdminByLoginAcct(userName);
// 2、获取该Admin对象的用户id
Integer adminId = admin.getId();
// 3、根据 adminId 获取 角色信息
List<Role> assignedRoles = roleService.getAssignedRole(adminId);
// 4、根据 adminId 获取权限信息
List<String> authNames = authService.getAssignedAuthNameByAdminId(adminId);
// 5、创建集合对象来存储 GrantedAuthority
List<GrantedAuthority> authorities = new ArrayList<>();
// 6、遍历assignedRoles存入角色信息
for (Role role : assignedRoles) {
// 不要忘记添加前缀
String roleName = "ROLE_" + role.getName();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleName);
authorities.add(simpleGrantedAuthority);
}
// 7、遍历 authNames 集合 存入权限信息
for (String authName : authNames) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authName);
authorities.add(simpleGrantedAuthority);
}
// 6、封装到 SecurityAdmin 对象中
return new SecurityAdmin(admin, authorities);
}
}
③ 在 SpringSecurity 配置类WebAppSecurityConfig 的 configure(AuthenticationManagerBuilder builder) 方法中调用
// 表示当前类是一个配置类
@Configuration
// 启用 web 环境下的权限控制功能
@EnableWebSecurity
// 启用全局方法权限控制功能,并设置 prePostEnabled = true ,
// 保证 @PreAuthority、@PostAuthority、@PreFilter、@PostFilter 注解生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CrowdUserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
// 使用基于数据库登录的账号、密码检测
// BCryptPasswordEncoder 盐值加密
builder.userDetailsService(userDetailsService).passwordEncoder(getBCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 对请求授权
.antMatchers("/admin-login-to-page.html") // 针对登录页进行设置
.permitAll() // 可以无条件访问
.antMatchers("/bootstrap/**") // 针对静态资源进行设置,可以无条件访问
.permitAll()
.antMatchers("/crowd/**")
.permitAll()
.antMatchers("/css/**")
.permitAll()
.antMatchers("/fonts/**")
.permitAll()
.antMatchers("/img/**")
.permitAll()
.antMatchers("/jquery/**")
.permitAll()
.antMatchers("/layer/**")
.permitAll()
.antMatchers("/script/**")
.permitAll()
.antMatchers("/ztree/**")
.permitAll()
.antMatchers("/admin-to-user-page.html") // 针对 Admin分页数据 设定访问控制
.hasRole("经理") // 具备“经理”角色
.anyRequest() // 其他任意请求
.authenticated() // 认证后才能访问
.and()
.csrf() // 防跨站请求伪造功能
.disable() // 禁用
.formLogin() // 使用表单形式登录
.loginPage("/admin-login-to-page.html") // 指定默认的登录页
.permitAll()
.loginProcessingUrl("/security-do-login-page.html") // 指定处理登录请求的地址
.permitAll()
.usernameParameter("login_acct") // 指定登录账号的 请求参数名
.passwordParameter("login_password") // 指定登录密码的 请求参数名
.defaultSuccessUrl("/admin-main-to-page.html") // 登录成功后前往的地址
.and()
.logout() // 开启退出功能
.logoutUrl("/security-do-logout-page.html") // 处理退出请求的地址
.logoutSuccessUrl("/admin-login-to-page.html") // 成功退出后页面跳转的地址
;
}
}
2、退出登录
2.1、禁用CSRF情况下退出
① 重写父类方法 指定处理退出请求的URL地址 和 成功退出后前往的地址 并禁用CSRF
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder
.inMemoryAuthentication() // 在内存中完成账号、密码的检查
.withUser("tom") // 指定账号
.password("123456") // 指定密码
.roles("ADMIN") // 指定角色
.and()
.withUser("jerry") // 指定账号
.password("123123") // 指定密码
.authorities("UPDATE") // 指定权限
;
}
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp 路径授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui/** 路径授权
.permitAll() // 可以无条件访问
.and()
.authorizeRequests() // 对请求进行授权
.anyRequest() // 任意请求
.authenticated() // 需要登录后才可以访问
.and()
.formLogin() // 使用表单形式登录
// 关于 loginPage() 方法的特殊说明
// 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
// /index.jsp GET - 去登录页
// /index.jsp POST - 提交登录的表单
// /index.jsp?error GET - 登录失败
// /index.jsp?logout GET - 退出登录
.loginPage("/index.jsp") // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)
// loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
.loginProcessingUrl("/do/login.html") // 指定提交表单的地址
.usernameParameter("loginAcct") // 指定登录账号的请求参数名
.passwordParameter("userPwd") // 指定登录密码的请求参数名
.defaultSuccessUrl("/main.html") // 指定登录成功后前往的地址
;
.and()
.csrf()
.disable() // 禁用CSRF
.logout() // 开启退出功能
.logoutUrl("/do/logout.html") // 指定处理退出请求的URL地址
.logoutSuccessUrl("/index.jsp") // 成功退出后前往的地址
;
}
}
② 表单退出按钮
<a href="${pageContext.request.contextPath }/do/logout.html">退出</a>
2.2、开启CSRF的情况下退出
① 重写父类方法 指定处理退出请求的URL地址 和 成功退出后前往的地址
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder
.inMemoryAuthentication() // 在内存中完成账号、密码的检查
.withUser("tom") // 指定账号
.password("123456") // 指定密码
.roles("ADMIN") // 指定角色
.and()
.withUser("jerry") // 指定账号
.password("123123") // 指定密码
.authorities("UPDATE") // 指定权限
;
}
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp 路径授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui/** 路径授权
.permitAll() // 可以无条件访问
.and()
.authorizeRequests() // 对请求进行授权
.anyRequest() // 任意请求
.authenticated() // 需要登录后才可以访问
.and()
.formLogin() // 使用表单形式登录
// 关于 loginPage() 方法的特殊说明
// 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
// /index.jsp GET - 去登录页
// /index.jsp POST - 提交登录的表单
// /index.jsp?error GET - 登录失败
// /index.jsp?logout GET - 退出登录
.loginPage("/index.jsp") // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)
// loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
.loginProcessingUrl("/do/login.html") // 指定提交表单的地址
.usernameParameter("loginAcct") // 指定登录账号的请求参数名
.passwordParameter("userPwd") // 指定登录密码的请求参数名
.defaultSuccessUrl("/main.html") // 指定登录成功后前往的地址
;
.and()
.logout() // 开启退出功能
.logoutUrl("/do/logout.html") // 指定处理退出请求的URL地址
.logoutSuccessUrl("/index.jsp") // 成功退出后前往的地址
;
}
}
② 表单退出按钮
<form id="logoutForm" action="${pageContext.request.contextPath }/do/logout.html" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
<a id="logoutAnchor" href="">退出</a>
<script type="text/javascript">
window.onload = function() {
// 给超链接的DOM对象绑定单击响应函数
document.getElementById("logoutAnchor").onclick = function() {
// 提交包含csrf参数的表单
document.getElementById("logoutForm").submit();
// 取消超链接的默认行为
return false;
};
};
</script>
3、基于角色或权限访问控制
① 基于配置类
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder
.inMemoryAuthentication() // 在内存中完成账号、密码的检查
.withUser("tom") // 指定账号
.password("123456") // 指定密码
.roles("ADMIN","学徒") // 指定角色
.and()
.withUser("jerry") // 指定账号
.password("123123") // 指定密码
.authorities("UPDATE","内门弟子") // 指定权限
;
}
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp 路径授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui/** 路径授权
.permitAll() // 可以无条件访问
.antMatchers("/level1/**") // 针对 /level1/** 路径设置访问要求
.hasRole("学徒") // 要求具备“学徒”角色才能访问
.antMatchers("/level2/**") // 针对 /level2/** 路径设置访问要求
.hasAuthority("内门弟子") // 要求具备“内门弟子”权限才能访问
.and()
.authorizeRequests() // 对请求进行授权
.anyRequest() // 任意请求
.authenticated() // 需要登录后才可以访问
.and()
.formLogin() // 使用表单形式登录
// 关于 loginPage() 方法的特殊说明
// 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
// /index.jsp GET - 去登录页
// /index.jsp POST - 提交登录的表单
// /index.jsp?error GET - 登录失败
// /index.jsp?logout GET - 退出登录
.loginPage("/index.jsp") // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)
// loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
.loginProcessingUrl("/do/login.html") // 指定提交表单的地址
.usernameParameter("loginAcct") // 指定登录账号的请求参数名
.passwordParameter("userPwd") // 指定登录密码的请求参数名
.defaultSuccessUrl("/main.html") // 指定登录成功后前往的地址
;
.and()
.logout() // 开启退出功能
.logoutUrl("/do/logout.html") // 指定处理退出请求的URL地址
.logoutSuccessUrl("/index.jsp") // 成功退出后前往的地址
;
}
}
!!!注意:通过观察源码不难发现,通过角色(role)来进行访问控制时,在传参的时候会自动的拼串加上"ROLE_"前缀,而通过权限(auth)控制则没有。
因此,将来从数据库查询得到的用户信息、角色信息、权 限信息需要我们自己手动组装。手动组装时需要我们自己给角色字符串前面加“ROLE_” 前缀。
② 基于注解
1、要在SpringSecurity的配置类上加上注解 @EnableGlobalMethodSecurity
// 表示当前类是一个配置类
@Configuration
// 启用 web 环境下的权限控制功能
@EnableWebSecurity
// 启用全局方法权限控制功能,并设置 prePostEnabled = true ,
// 保证 @PrePreAuthorize、@PostPreAuthorize、@PreFilter、@PostFilter 注解生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
2、在要设置权限访问的请求上添加注解 @PreAuthorize()
@PreAuthorize("hasAuthority('user:save')") // 具备 user:save 权限才能访问
@RequestMapping("/admin-save.html")
public String saveAdmin(Admin admin) {
adminService.saveAdmin(admin);
return "redirect:/admin-to-user-page.html?pageNum=" + Integer.MAX_VALUE;
}
@PreAuthorize("hasRole('部长')") // 具备部长角色才能访问 Role分页数据页面
@ResponseBody
@RequestMapping("/role-get-page-info.json")
public ResultEntity<PageInfo<Role>> getPageInfo(
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "5") Integer pageSize,
@RequestParam(value = "keyword", defaultValue = "") String keyword
) {
// 调用service 里的方法获取数据信息
PageInfo<Role> pageInfo = roleService.getPageInfo(pageNum, pageSize, keyword);
// 封装到 ResultEntity 对象里
return ResultEntity.successWithData(pageInfo);
}
4、自定义403异常页面
4.1、重写父类方法
指定访问被拒绝时前往的页面
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.exceptionHandling() // 指定异常处理器
// .accessDeniedPage("/to/no/auth/page.html") // 访问被拒绝时前往的页面
.accessDeniedHandler(new AccessDeniedHandler() { // 自定义 细节
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletRequest.setAttribute("message","抱歉!您的权限不够!☆☆☆☆☆☆☆");
httpServletRequest.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(httpServletRequest,httpServletResponse);
}
})
;
}
}
4.2、编写自定义403异常页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
pageContext.setAttribute("PATH", request.getContextPath());
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1">
<title>武林秘籍管理系统</title>
<link rel="stylesheet" href="${PATH }/layui/css/layui.css">
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
<!-- 顶部导航 -->
<%@include file="/WEB-INF/include/navbar.jsp" %>
<!-- 侧边栏 -->
<%@include file="/WEB-INF/include/sidebar.jsp" %>
<div class="layui-body">
<!-- 内容主体区域 -->
<div style="padding: 15px;">
<h1>非常抱歉!您没有访问这个功能的权限!</h1>
<h2>${message }</h2>
</div>
</div>
<div class="layui-footer"></div>
</div>
<script src="${PATH }/layui/layui.js"></script>
<script>
//JavaScript代码区域
layui.use('element', function() {
var element = layui.element;
});
</script>
</body>
</html>
5、记住我
5.1、内存版
HttpSecurity 对象调用 rememberMe()方法。 登录表单携带名为 remember-me 的请求参数。具体做法是将登录表单中的 checkbox 的 name 设置为 remember-me.
如 果 不 能 使 用 “ remember-me ” 作 为 请 求 参 数 名 称 , 可 以 使 用HttpSecurity 对象调用 rememberMeParameter()方法定制。
① 重写父类方法,开启记住我功能
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity security) throws Exception {
security.rememberMe(); // 开启“记住我”功能
}
}
② 页面勾选框“记住我”
<input type="checkbox" name="remember-me" lay-skin="primary"
title="记住我"> <a href="forget.html"
class="layadmin-user-jump-change layadmin-link"
style="margin-top: 7px;">忘记密码?</a>
5.2、数据库版
为了让服务器重启也不影响记住登录状态,将用户登录状态信息存入数据库。
① 在此之前,需要导入数据库相关依赖以及在spring-mvc.xml中配置数据源
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="tianfei"></property>
<property name="url" value="jdbc:mysql://localhost:3306/security?useSSL=false"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
<!-- jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
② 创建数据库
CREATE DATABASE security
CHARACTER SET utf8;
③ 在 WebAppSecurityConfig 类中注入数据源
@Autowired private
DataSource dataSource;
!!!注意:在JdbcTokenRepositoryImpl 类中由于创建表的initDao()函数是protected,不能再外部调用,所以要进行一定的操作(有三种方法):
- 可以写一个类继承JdbcTokenRepositoryImpl 类
- 修改JdbcTokenRepositoryImpl 类的源码
- 自己创建同结构的表
我这里选择的是修改源码(不建议)
④ 修改的源码部分(没写出来的部分不用修改)如下
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
public static final String CREATE_TABLE_SQL = "create table if not exists persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)";
public void initDao() {
if (this.createTableOnStartup) {
this.getJdbcTemplate().execute("create table if not exists persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)");
}
}
}
⑤ 具体的SpringSecurity配置类WebAppSecurityConfig 代码如下
/**
*
* SpringSecurity的配置类
*
* @author: Herz
* @date: 2021/7/19 12:43
*/
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
// 装配 userDetailsService
builder
.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity security) throws Exception {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 开启自动创建表功能
tokenRepository.setCreateTableOnStartup(true);
// 自动创建表方法
tokenRepository.initDao();
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp 路径授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui/** 路径授权
.permitAll() // 可以无条件访问
.antMatchers("/level1/**") // 针对 /level1/** 路径设置访问要求
.hasRole("学徒") // 要求具备“学徒”角色才能访问
.antMatchers("/level2/**") // 针对 /level2/** 路径设置访问要求
.hasAuthority("内门弟子") // 要求具备“内门弟子”权限才能访问
.and()
.authorizeRequests() // 对请求进行授权
.anyRequest() // 任意请求
.authenticated() // 需要登录后才可以访问
.and()
.formLogin() // 使用表单形式登录
// 关于 loginPage() 方法的特殊说明
// 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
// /index.jsp GET - 去登录页
// /index.jsp POST - 提交登录的表单
// /index.jsp?error GET - 登录失败
// /index.jsp?logout GET - 退出登录
.loginPage("/index.jsp") // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)
// loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
.loginProcessingUrl("/do/login.html") // 指定提交表单的地址
.usernameParameter("loginAcct") // 指定登录账号的请求参数名
.passwordParameter("userPwd") // 指定登录密码的请求参数名
.defaultSuccessUrl("/main.html") // 指定登录成功后前往的地址
// .and()
// .csrf()
// .disable() // 禁用CSRF
.and()
.logout() // 开启退出功能
.logoutSuccessUrl("/index.jsp") // 成功退出后前往的地址
.and()
.exceptionHandling() // 指定异常处理器
// .accessDeniedPage("/to/no/auth/page.html") // 访问被拒绝时前往的页面
.accessDeniedHandler(new AccessDeniedHandler() { // 自定义 细节
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletRequest.setAttribute("message","抱歉!您的权限不够!☆☆☆☆☆☆☆");
httpServletRequest.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(httpServletRequest,httpServletResponse);
}
})
.and()
.rememberMe() // 开启“记住我”功能
.tokenRepository(tokenRepository)
;
}
}
6、登录密码加密
6.1、普通加密
① 编写一个类继承 PasswordEncoder 类 并实现抽象方法
/**
*
* 用于登录密码检测
*
* @author: Herz
* @date: 2021/7/20 9:47
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
/**
* 给登录密码加密操作
* @param rowPassword 登录密码
* @return
*/
@Override
public String encode(CharSequence rowPassword) {
try {
// 1、创建 MessageDigest 对象
String algorithm = "MD5";
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
// 2、获取 rowPassword 的字节数组
byte[] input = ((String) rowPassword).getBytes();
// 3、加密
byte[] output = messageDigest.digest(input);
// 4、转换为16进制数对应的字符 大写
String encoded = new BigInteger(1, output).toString(16).toUpperCase();
return encoded;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
/**
* 比较登录密码和数据库密码是否一致
* @param rowPassword 登录明文密码
* @param encodePassword 数据库加密密码
* @return
*/
@Override
public boolean matches(CharSequence rowPassword, String encodePassword) {
// 1、明文密码加密
String formPassword = encode(rowPassword);
// 2、声明数据库密码
String dataBasePassword = encodePassword;
// 3、比较密码是否一致
return Objects.equals(formPassword,dataBasePassword);
}
}
②在 SpringSecurity 配置类中 通过 AuthenticationManagerBuilder 对象 调用 userDetailsService(userDetailsService).passwordEncoder(passwordEncoder) 方法
/**
*
* SpringSecurity的配置类
*
* @author: Herz
* @date: 2021/7/19 12:43
*/
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private MyPasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
// 装配 userDetailsService
builder
.userDetailsService(userDetailsService) // 用于用户登录检测
.passwordEncoder(passwordEncoder); // 普通密码加密
}
6.2、带盐值加密
直接在 SpringSecurity 配置类中 通过 AuthenticationManagerBuilder 对象 调用 userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()) 方法
/**
*
* SpringSecurity的配置类
*
* @author: Herz
* @date: 2021/7/19 12:43
*/
// 表示当前类是一个配置类
@Configuration
// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private MyPasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
// 装配 userDetailsService
builder
.userDetailsService(userDetailsService) // 用于用户登录检测
.passwordEncoder(new BCryptPasswordEncoder()); // 带盐值密码加密
}
7、页面元素的访问控制
例如:
① 在页面引入标签库
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
② 给页面元素添加角色或权限
<%-- 这里设置的是角色
设置权限:<security:authorize access="hasAuthority('role:add')">
--%>
<security:authorize access="hasRole('经理')">
<div class="col-xs-6 col-sm-3 placeholder">
<img data-src="holder.js/200x200/auto/sky" class="img-responsive"
alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</security:authorize>
效果:使用不具备该角色的用户登录时查看不到该页面元素