SpringSecurity 在 SSM架构中的使用
SpringSecurity - SSM
SpringSecurity 对比 Shiro
SpringSecurity的特点:
- 能和 Spring无缝贴合
- 能实现全面的权限控制
- 专门为 Web开发而设计
- 旧版本不能脱离 Web环境使用
- 新版本单独引入核心模块可以脱离 Web环境使用
- 重量级
Shiro的特点:
- 轻量级
- 通用性
- 但是在 Web环境下某些特定的需求需要手动编写代码定制
首先搭建 Web环境
引入 Web依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.20.RELEASE</version>
</dependency>
<!-- 引入 Servlet 容器中相关依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!-- JSP 页面使用的依赖 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1.3-b06</version>
<scope>provided</scope>
</dependency>
</dependencies>
创建 SpringMVC
的配置文件
<context:component-scan
base-package="com.atguigu.security"></context:component-scan>
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler />
修改 web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
逐步深入 SpringSecurity
加入 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>
加入 SpringSecurity
权限控制的 Filter
SpringSecurity
使用的是过滤器Filter
而不是拦截器Interceptor
- 意味着
SpringSecurity
能够管理的不仅仅是SpringMVC
中的controller
请求,还包含 Web 应用中所有请求。 - 比如:项目中的静态资源也会被拦截,从而进行权限控制。
- 意味着
修改 web.xml
<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>
注意: <filter-name>springSecurityFilterChain</filter-name>
标 签 中 必 须 是 springSecurityFilterChain
。
加入 WebSecurity
的配置类
@Configuration
@EnableWebSecurity // 开启 SpringSecurity关于 Web项目的注解支持
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
实验 1:放行首页和静态资源
在配置类中重写父类的 configure(HttpSecurity security)
方法
protected void configure(HttpSecurity security) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially
override subclass configure(HttpSecurity).");
security
.authorizeRequests()
.anyRequest().authenticated() //所有请求都需要进行认证
.and()
.formLogin()
.and()
.httpBasic();
}
重写后:
@Override
protected void configure(HttpSecurity security) throws Exception {
//super.configure(security); 注释掉将取消父类方法中的默认规则
security.authorizeRequests() //对请求进行授权
.antMatchers("/layui/**","/index.jsp") //使用 ANT 风格设置要授权的 URL 地址
.permitAll() //允许上面使用 ANT 风格设置的全部请求
.anyRequest() //其他未设置的全部请求
.authenticated(); //需要认证
}
实验 2:未认证请求跳转到登录页
@Override
protected void configure(HttpSecurity security) throws Exception {
//super.configure(security); 注释掉将取消父类方法中的默认规则
security.authorizeRequests() //对请求进行授权
.antMatchers("/layui/**","/index.jsp") //使用 ANT 风格设置要授权的 URL 地
址
.permitAll() //允许上面使用 ANT 风格设置的全部请求
.anyRequest() //其他未设置的全部请求
.authenticated() //需要认证
.and()
.formLogin() //设置未授权请求跳转到登录页面
.loginPage("/index.jsp") //指定登录页
.permitAll() //为登录页设置所有人都可以访问
// loginProcessingUrl()方法指定了登录地址,就会覆盖 loginPage()方法中设置的默认值 index.jsp POST
.loginProcessingUrl("/do/login.html") // 指定提交登录表单的地址
;
}
实验 3:设置登录系统的账号、密码
SpringSecurity
默认账号的请求参数名:username
SpringSecurity
默认密码的请求参数名:password
为了让 SpringSecurity
接管用户登陆,要么修改页面上的表单项的 name 属性值,要么修改配置。
可以通过如下方法修改配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.usernameParameter("xxxName") // 定制登录账号的请求参数名
.passwordParameter("xxxPswd") // 定制登录密码的请求参数名
.defaultSuccessUrl("/main.html"); //设置登录成功后默认前往的 URL 地址
;
}
重写另外一个父类的方法,来设置登录系统的账号密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth); 一定要禁用默认规则
auth
.inMemoryAuthentication()
.withUser("tom").password("123123") //设置账号密码
.roles("ADMIN") //设置角色
.and()
.withUser("jerry").password("456456")//设置另一个账号密码
.authorities("SAVE","EDIT"); //设置权限
}
Cannot pass a null GrantedAuthority collection 问 题 是 由 于 没 有 设 置 roles() 或 authorities()方法导致的
注意:登陆时需要携带参数 <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
当然,也可以禁用 csrf
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable() // 禁用 csrf
;
}
实验 4:用户注销
通过调用 HttpSecurity
对象的一系列方法设置注销功能。
logout()
方法:开启注销功能logoutUrl()
方法:自定义注销功能的 URL 地址logoutSuccessUrl()
方法:退出成功后前往的 URL 地址addLogoutHandler()
方法:添加退出处理器logoutSuccessHandler()
方法:退出成功处理器
如果 CSRF 功能没有禁用,那么退出请求必须是 POST 方式。如果禁用了 CSRF功能则任何请求方式都可以。
实验 5:基于角色或权限进行访问控制
通过 HttpSecurity
对象设置资源的角色要求
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //对请求进行授权
.antMatchers("/layui/**","/index.jsp") //使用 ANT 风格设置要授权的 URL 地址
.permitAll() //允许上面使用 ANT 风格设置的全部请求
.antMatchers("/level1/**")
.hasRole("学徒")
.antMatchers("/level2/**")
.hasRole(" 大师")
.antMatchers("/level3/**")
.hasRole(" 宗师")
.anyRequest() //其他未设置的全部请求
.authenticated() //需要认证
.and()
.formLogin() //设置未授权请求跳转到登录页面:开启表单登录功
能
.loginPage("/index.jsp") //指定登录页
.permitAll() //为登录页设置所有人都可以访问
.defaultSuccessUrl("/main.html") //设置登录成功后默认前往的 URL 地址
.and()
.logout()
.logoutUrl("/my/logout")
.logoutSuccessUrl("/index.jsp")
;
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("tom").password("123123") //设置账号密码
.roles("ADMIN","学徒","宗师") //设置角色
.and()
.withUser("jerry").password("456456")//设置另一个账号密码
.authorities("SAVE","EDIT"); //设置权限
}
类似异常的 catch顺序,路径的声明先后也很重要,大的路径声明在后面,小的在前面
实验 6 :自定义 403 错误页面
protected void configure(HttpSecurity http) throws Exception {
http
.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);
}
})
;
}
实验 7 :记住我-内存版
登录表单携带名为 remember-me
的请求参数
如果不能使用 remember-me
作为请求参数名称,可以使用 rememberMeParameter()
方法定制
protected void configure(HttpSecurity http) throws Exception {
http
.remeberMe(); // 开启记住我的功能
}
通过开发者工具看到浏览器端存储了名为remember-me的Cookie。根据这个Cookie 的 value 在服务器端找到以前登录的 User。
而且这个 Cookie 被设置为存储 2 个星期的时间。
实验 8 :记住我-数据库版
为了让服务器重启也不影响记住登录状态,将用户登录状态信息存入数据库。
需要在 WebAppSecurityConfig
类中注入数据源
@Autowired
private DataSource dataSource;
@Configuration
@EnableWebSecurity // 启用 Web安全处理功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
protected void configure(HttpSecurity http) throws Exception {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
http
.rememberMe()
.tokenRepository(tokenRepository);
}
}
注意:需要进入 JdbcTokenRepositoryImpl 类中找到创建 persistent_logins 表的 SQL 语句创建persistent_logins 表。
CREATE TABLE persistent_logins (
username VARCHAR (64) NOT NULL,
series VARCHAR (64) PRIMARY KEY,
token VARCHAR (64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
实验 9:查询数据库完成认证
创建存储用户的表
create table t_admin (
id int not null auto_increment,
loginacct varchar(255) not null,
userpswd char(32) not null,
username varchar(255) not null,
email varchar(255) not null,
createtime char(19),
primary key (id)
);
需要实现 UserDetailsService
接口,重写 loadUserByUsername()
方法加载用户
package com.atguigu.security.service;
/**
* @author :OliQ
* @date :Created on 2021/8/27 15:50
*/
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public UserDetails loadUserByUsername(
// 表单提交的用户名
String s) throws UsernameNotFoundException {
String sql = "SELECT id, loginacct, userpswd, username, email, createtime FROM t_admin WHERE loginacct = ?";
// 执行 sql语句,获取查询结果
Map<String, Object> resultMap = jdbcTemplate.queryForMap(sql, s);
// 获取用户名、密码等数据
String loginacct = resultMap.get("loginacct").toString();
String userpswd = resultMap.get("userpswd").toString();
// 创建权限列表,如果是角色的话就需要手动拼接 ROLE_前缀
List<GrantedAuthority> list = AuthorityUtils.createAuthorityList("ADMIN", "USER", "ROLE_学徒");
// 传入用户名和密码,创建 User对象并返回
org.springframework.security.core.userdetails.User user
= new org.springframework.security.core.userdetails.User(loginacct, userpswd, list);
return user;
}
}
在 WebSecurityConfig
中使用自定义的 UserDetailsService
完成登陆
@Configuration
@EnableWebSecurity // 启用 Web安全处理功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
;
}
}
在存储 GrantedAuthority
的列表中,区分 Role
和 Authority
的唯一准则是 Role
有 Role_
的前缀,所以在使用自定义的 UserDetailsService
登录时,在给用户添加权限时,处理 Role
时要手动添加前缀。而使用内存版的时候,不能添加前缀。
实验 10:应用自定义密码加密规则
首先得保证数据库中存储的用户密码就是加密了的
SpringSecurity
默认使用 BCryptPasswordEncoder
实现密码的加解密。
BCryptPasswordEncoder
在加密时通过加入随机 盐值让每一次的加密结果都不同。能够避免密码的明文被猜到。
而在对明文和密文进行比较时,BCryptPasswordEncoder
会在密文的固定位置取出盐值,重新进行加密。
使用方法也很简单,只需要在 WebAppSecurityConfig
中添加配置即可
@Configuration
@EnableWebSecurity // 启用 Web安全处理功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService) // 使用从数据库读取出来的用户相关信息
.passwordEncoder(passwordEncoder) // 设置加密方式
;
}
}
实验11:在页面上显示用户昵称
在 JSP中导入标签库
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
通过标签获取已登录的用户信息
<security:authentication property="principal.originalAdmin.userName"/>
实验12:权限控制
通过配置类设置某一路径的访问权限
protected void configure(HttpSecurity http) throws Exception {
http
.antMatchers("/admin/get/page.html")// 针对分页显示 Admin 数据设定访问控制
.hasRole("经理") // 要求具备经理角色
}
通过注解设置权限
@PreAuthorize("hasRole(' 部长')")
@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
) {
PageInfo<Role> pageInfo = roleService.getPageInfo(pageNum, pageSize, keyword);
return ResultEntity.successWithData(pageInfo);
}
同时需要在 WebAppSecurityConfig
类中添加注解
// 启用全局方法权限控制功能,并且设置 prePostEnabled = true。保证@PreAuthority、
// @PostAuthority、@PreFilter、@PostFilter 生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
hasAuthority也是一样的,在注解中可以这么写:
@PreAuthorize("hasRole(' 部长') OR hasAuthority('xxx')")
类似的还有 @PostAuthorize()
注解,先执行方法然后根据方法返回值判断是否具备权限
例如:查询一个 Admin 对象,在@PostAuthorize 注解中和当前登录的 Admin 对象进行比较,如果不一致,则判断为不能访问。
实现“只能查自己”效果。@PostAuthorize("returnObject.data.loginAcct == principal.username")
还有 @PreFilter
注解,在方法执行前对传入的参数进行过滤。只能对集合类型的数据进行过滤。
@PostFilter
注解,在方法执行后对方法返回值进行过滤。只能对集合类型的数据进行过滤。
实验13:目标 9:页面元素的权限控制
针对页面上的局部元素,根据访问控制规则进行控制
在 JSP页面中,在需要被控制的元素外使用如下标签包裹
<security:authorize access="hasRole('经理')">
<!-- 开始和结束标签之间是要进行权限控制的部分。检测当前用户是否有权限,有权限
就显示这里的内容,没有权限就不显示。 -->
……
</security:authorize>