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的列表中,区分 RoleAuthority的唯一准则是 RoleRole_的前缀,所以在使用自定义的 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>

SpringSecurity 在 SSM架构中的使用的更多相关文章

  1. 将SSM架构中原来关于springSecurity3.x版本的写法配迁移到SpringBoot2.0框架中出现的问题解决记

    迁移过程中关于这个安全框架的问题很麻烦,springBoot自带的stater中的版本是5.0,原来系统有通过实现"org.springframework.security.authenti ...

  2. 认证鉴权与API权限控制在微服务架构中的设计与实现(四)

    引言: 本文系<认证鉴权与API权限控制在微服务架构中的设计与实现>系列的完结篇,前面三篇已经将认证鉴权与API权限控制的流程和主要细节讲解完.本文比较长,对这个系列进行收尾,主要内容包括 ...

  3. SSM框架中的前后端分离

    认识前后端分离 在传统的web应用开发中,大多数的程序员会将浏览器作为前后端的分界线.将浏览器中为用户进行页面展示的部分称之为前端,而将运行在服务器,为前端提供业务逻辑和数据准备的所有代码统称为后端. ...

  4. 手把手带你撸一把springsecurity框架源码中的认证流程

    提springsecurity之前,不得不说一下另外一个轻量级的安全框架Shiro,在springboot未出世之前,Shiro可谓是颇有统一J2EE的安全领域的趋势. 有关shiro的技术点 1.s ...

  5. 分享一个CQRS/ES架构中基于写文件的EventStore的设计思路

    最近打算用C#实现一个基于文件的EventStore. 什么是EventStore 关于什么是EventStore,如果还不清楚的朋友可以去了解下CQRS/Event Sourcing这种架构,我博客 ...

  6. 大型 JavaScript 应用架构中的模式

    原文:Patterns For Large-Scale JavaScript Application Architecture by @Addy Osmani 今天我们要讨论大型 JavaScript ...

  7. [转]大型 JavaScript 应用架构中的模式

    目录 1.我是谁,以及我为什么写这个主题 2.可以用140个字概述这篇文章吗? 3.究竟什么是“大型”JavaScript应用程序? 4.让我们回顾一下当前的架构 5.想得长远一些 6.头脑风暴 7. ...

  8. 三层架构中bll层把datatable转换为实体model的理解

    看了很多人的项目,很多都是用到三层架构,其中BLL层中有一种将DataTable转换为实体的方法.一直没有明白为啥要这样做,今天特意去搜索了一下,如果没有答案我是准备提问,寻求解答了.还好找到一个相关 ...

  9. 谈谈三层架构中Model的作用

    Model又叫实体类,这个东西,大家可能觉得不好分层.包括我以前在内,是这样理解的:UI<-->Model<-->BLL<-->Model<-->DAL ...

随机推荐

  1. 2022-7-25 第七组 pan小堂 多态

    多态 多态是继封装.继承之后,面向对象的第三大特性. 现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个具体的同学张三既是学生也是人,即出现两种形态. Java作为面向对象的语言,同样可以描 ...

  2. DQL排序查询和DQL聚合函数

    DQL:查询语句 排序查询 语法: order by 字句 order by 排序字段1 排序方式1,排序字段2 排序方式2... 排序方式: ASC:升序,默认的 DESC:降序 SELECT * ...

  3. redis学习之数据类型

    <?php //连接本地的 Redis 服务 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); echo "Co ...

  4. 一代版本一代神:利用Docker在Win10系统极速体验Django3.1真实异步(Async)任务

    一代版本一代神:利用Docker在Win10系统极速体验Django3.1真实异步(Async)任务 原文转载自「刘悦的技术博客」https://v3u.cn/a_id_177 就在去年(2019年) ...

  5. Fishhole类定义和实现

    当眼睛处于水中,产生类似的鱼眼视角,fov永远是psi_max的2倍.具体算法参考书籍. 类声明: #pragma once #ifndef __FISHHOLE_HEADER__ #define _ ...

  6. Luogu1064 金明的预算方案 (有依赖的背包)

    枚举多个状态 #include <iostream> #include <cstdio> #include <cstring> #include <algor ...

  7. Redis安装及常用配置

    Redis安装说明 大多数企业都是基于Linux服务器来部署项目,而且Redis官方也没有提供Windows版本的安装包.因此课程中我们会基于Linux系统来安装Redis. 此处选择的Linux版本 ...

  8. matery添加加载动画

    1.在主题 /layout/_partial/目录新建一个loading-pages.ejs 内容如下: <style type="text/css" lang=" ...

  9. JS/java实现QQ空间自动点赞

    使用方法: 1:进入QQ空间 2:复制下面代码 3:按F12或右键审查元素 进入控制台 也就是console 4:粘贴  回车键  喝口水 5:如果嫌慢的话可以 修改这段代码. window.setI ...

  10. Springboot log4j2总结

    Log4j2 PS: Log4j 已不再维护,而最新的是Log4j2, Log4j2 是全部重写了Log4j,并拥有更加优秀的性能 1. 引入依赖,和去掉logging的依赖 <dependen ...