目录
1.2、在 web.xml 中配置 DelegatingFilterProxy
1.4、谁来把 WebAppSecurityConfig 扫描到 IOC 里?
1.5.2、DelegatingFilterProxy 查找 IOC 容器然后查找 bean 的工作机制
2.11.2、测试一:访问 Admin 分页时具备“经理”角色
2.11.3、测试二:访问 Role 的分页时具备“部长”角色
2.11.4、测试三:要求:访问 Admin 分页功能时具备“经理”角色或“user:get”权限二者之一
2.11.5、测试四:访问 Admin 保存功能时具备 user:save 权限
1、加入 Spring-Security 环境
1.1、加入依赖
在 atcrowdfunding01-admin-parent 模块和 atcrowdfunding05-common-util 模块的 pom.xml 加入依赖
<!-- 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>
1.2、在 web.xml 中配置 DelegatingFilterProxy
<!--
注意:SpringSecurity 会根据 DelegatingFilterProxy 的 filter-name 到 IOC 容器中
查找所需要的 bean。所以 filter-name 必须是 springSecurityFilterChain 名字。
-->
<filter>
<!-- filter-name 标签必须为springSecurityFilterChain -->
<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>
1.3、创建基于注解的配置类
package com.atguigu.crowd.mvc.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author zhang
* @Date 2022/6/6 - 16:18
* @Version 1.0
*/
@Configuration // 表示当前类是一个配置类
@EnableWebSecurity // 启动Web环境下权限控制功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
1.4、谁来把 WebAppSecurityConfig 扫描到 IOC 里?
如果是 Spring 的 IOC 容器扫描:
如果是 SpringMVC 的 IOC 容器扫描:
结论:为了让 SpringSecurity 能够针对浏览器请求进行权限控制,需要让 SpringMVC 来扫描 WebAppSecurityConfig 类。
衍生问题:DelegatingFilterProxy 初始化时需要到 IOC 容器查找一个 bean, 这个 bean 所在的 IOC 容器要看是谁扫描了 WebAppSecurityConfig。
如果是 Spring 扫描了 WebAppSecurityConfig,那么 Filter 需要的 bean 就在 Spring 的 IOC 容器。
如果是 SpringMVC 扫描了 WebAppSecurityConfig,那么 Filter 需要的 bean 就在 SpringMVC 的 IOC 容器。
1.5、找不到 bean 的问题分析
06-Jun-2022 17:25:51.237 严重 [RMI TCP Connection(3)-127.0.0.1] org.apache.catalina.core.StandardContext.filterStart 启动过滤器异常
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' available
1.5.1、明确三大组件启动顺序
首先:ContextLoaderListener 初始化,创建 Spring 的 IOC 容器
其次:DelegatingFilterProxy 初始化,查找 IOC 容器、查找 bean
最后:DispatcherServlet 初始化,创建 SpringMVC 的 IOC 容器
1.5.2、DelegatingFilterProxy 查找 IOC 容器然后查找 bean 的工作机制
1.5.3、解决方法一:改源码
① 创建与 DelegatingFilterProxy 类相同的包,在包下创建 DelegatingFilterProxy 类,其内容与源码的一样
② 设置初始化时直接跳过查找 IOC 容器的环节
将下图红框代码注释
③ 第一次请求时直接找 SpringMVC 的 IOC 容器
修改 doFilter 方法,位置如下图
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
// 把原来查找IOC容器的代码注释
// WebApplicationContext wac = findWebApplicationContext();
// 按我们自己的需要重新编写
// 获取 ServletContext 对象
ServletContext sc = this.getServletContext();
// 拼接 SpringMVC 将 IOC 容器存入 ServletContext 域的时候使用的属性名
String servletName = "springDispatcherServlet";
String attrName = FrameworkServlet.SERVLET_CONTEXT_PREFIX + servletName;
// 根据 attrName 从 ServletContext 域中获取 IOC 容器对象
WebApplicationContext wac = (WebApplicationContext) sc.getAttribute(attrName);
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
1.5.4、解决方法二:把两个 IOC 容器合二为一
不使用 ContextLoaderListener,让 DispatcherServlet 加载所有 Spring 配置文件。
方法:取消 web.xml 中配置 ContextLoaderListener 并加载所有 spring 的配置文件
缺点:会破坏现有程序的结构。原本是 ContextLoaderListener 和 DispatcherServlet 两个组件创建两个 IOC 容器,现在改成只有一个。
2、目标
2.1、放行登录页和静态资源
在 SpringSecurity 的配置类中配置
@Configuration // 表示当前类是一个配置类
@EnableWebSecurity // 启动Web环境下权限控制功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 对请求进行授权
.antMatchers("/admin/to/login/page.html","/bootstrap/**","/script/**","/ztree/**",
"/crowd/**","/css/**","/fonts/**","/img/**","/jquery/**","/layer/**") // 对登录页及静态资源设置
.permitAll() // 无条件访问
.anyRequest() // 对所有请求设置
.authenticated() // 需要认证
;
}
}
2.2、提交登录表单做内存认证
① 设置 admin-login.jsp 页面的表单,修改表单提交的地址为 action="security/do/login.html",添加错误信息显示
action="security/do/login.html"
<p>${SPRING_SECURITY_LAST_EXCEPTION.message }</p>
② 将之前在 spring-web-mvc.xml 的拦截器注释
③ SpringSecurity 配置
在 configure(HttpSecurity http) 进行设置
.csrf() // 防跨站请求伪造功能
.disable() // 禁用csrf
.formLogin() // 开启表单登录功能
.loginPage("/admin/to/login/page.html") // 指定登录页
.loginProcessingUrl("/security/do/login.html") // 指定处理登录请求的地址
.defaultSuccessUrl("/admin/to/main/page.html") // 指定登录成功后前往的地址
.usernameParameter("loginAcct") // 表单中账号的请求参数名
.passwordParameter("userPswd") // 表单中密码的请求参数名
在 configure(AuthenticationManagerBuilder auth) 设置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("tom")
.password("123123")
.roles("ADMIN")
;
}
2.3、退出登录
① 在 include-nav.jsp 修改退出登录的超链接
② 在 SpringSecurity 的配置类的 configure(HttpSecurity http) 方法设置
.logout() // 开启退出登录功能
.logoutUrl("/security/do/logout.html") // 指定退出登录地址
.logoutSuccessUrl("/admin/to/login/page.html") // 指定退出成功后前往的地址
2.4、把内存登录改成数据库登录
① 根据 adminId 查询已分配的角色,这个操作在之前已经写好
② 根据 adminId 查询已分配权限
AuthMapper.xml
<select id="selectAssignedAuthNameByAdminId" resultType="java.lang.String">
SELECT DISTINCT `t_auth`.`name`
FROM `t_auth`
LEFT JOIN `inner_role_auth` ON `t_auth`.`id` = `inner_role_auth`.`auth_id`
LEFT JOIN `inner_admin_role` ON `inner_admin_role`.`role_id` = `inner_role_auth`.`role_id`
WHERE `inner_admin_role`.`admin_id` = #{adminId} AND `t_auth`.`name` != "" AND `t_auth`.`name` IS NOT NULL
</select>
AuthMapper
/**
* 根据adminId查询已分配的权限
* @param adminId
* @return
*/
List<String> selectAssignedAuthNameByAdminId(Integer adminId);
AuthService
/**
* 根据adminId查询已分配的权限
* @param adminId
* @return
*/
List<String> getAssignedAuthNameByAdminId(Integer adminId);
AuthServiceImpl
/**
* 根据adminId查询已分配的权限
* @param adminId
* @return
*/
@Override
public List<String> getAssignedAuthNameByAdminId(Integer adminId) {
return authMapper.selectAssignedAuthNameByAdminId(adminId);
}
③ 创建 SecurityAdmin 类
package com.atguigu.crowd.mvc.config;
import com.atguigu.crowd.entity.Admin;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.List;
/**
* @Author zhang
* @Date 2022/6/8 - 19:27
* @Version 1.0
*/
// 考虑到User对象中仅仅包含账号和密码,为了能够获取到原始的Admin对象,专门创建这个类对User类进行扩展
public class SecurityAdmin extends User {
private static final long serialVersionUID = 1L;
// 原始的Admin对象,包含Admin对象的全部属性
private Admin originalAdmin;
/**
* 构造器
* @param originalAdmin 原始的Admin对象
* @param authorities 该对象的角色、权限信息的集合
*/
public SecurityAdmin(Admin originalAdmin, List<GrantedAuthority> authorities){
// 调用父类构造器
super(originalAdmin.getLoginAcct(), originalAdmin.getUserPswd(), authorities);
this.originalAdmin = originalAdmin;
}
public Admin getOriginalAdmin() {
return originalAdmin;
}
}
④ 根据账号查询 Admin
AdminService
/**
* 根据账号查询admin
* @param username
* @return
*/
Admin getAdminByLoginAcct(String username);
AdminServiceImpl
/**
* 根据账号查询admin
* @param username
* @return
*/
@Override
public Admin getAdminByLoginAcct(String username) {
AdminExample adminExample = new AdminExample();
AdminExample.Criteria criteria = adminExample.createCriteria();
criteria.andLoginAcctEqualTo(username);
List<Admin> admins = adminMapper.selectByExample(adminExample);
Admin admin = admins.get(0);
return admin;
}
⑤ 创建 UserDetailsService 实现类
package com.atguigu.crowd.mvc.config;
import com.atguigu.crowd.entity.Admin;
import com.atguigu.crowd.entity.Role;
import com.atguigu.crowd.service.api.AdminService;
import com.atguigu.crowd.service.api.AuthService;
import com.atguigu.crowd.service.api.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @Author zhang
* @Date 2022/6/8 - 19:35
* @Version 1.0
*/
@Component
public class CrowdUserDetailsService implements UserDetailsService {
@Autowired
private AdminService adminService;
@Autowired
private RoleService roleService;
@Autowired
private AuthService authService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据账号名称查询Admin对象
Admin admin = adminService.getAdminByLoginAcct(username);
// 获取adminId
Integer adminId = admin.getId();
// 根据adminId查询角色信息
List<Role> assignedRoleList = roleService.getAssignedRole(adminId);
// 根据adminId查询权限信息
List<String> authNameList = authService.getAssignedAuthNameByAdminId(adminId);
// 存入角色和权限信息
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : assignedRoleList) {
String roleName = "ROLE_" + role.getName();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleName);
authorities.add(simpleGrantedAuthority);
}
for (String authName : authNameList) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authName);
authorities.add(simpleGrantedAuthority);
}
// 封装
SecurityAdmin securityAdmin = new SecurityAdmin(admin, authorities);
return securityAdmin;
}
}
⑥ 在配置类中使用 UserDetailsService
先注入 UserDetailsService,再在 configure(AuthenticationManagerBuilder auth) 方法使用
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用内存登录
//auth
// .inMemoryAuthentication()
// .withUser("tom")
// .password("123123")
// .roles("ADMIN")
// 使用数据库登录
auth
.userDetailsService(userDetailsService)
;
}
2.5、密码加密
① 修改 t_admin 表结构
由于使用带盐值的加密方式,生成的密文长度超过之前定义的长度,所以要修改数据表的密码长度
ALTER TABLE `project_crowd`.`t_admin` CHANGE `user_pswd` `user_pswd` CHAR(100) CHARSET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '登录密码 ';
② 使用 BCryptPasswordEncoder 对象
一、若在加入 SpringSecurity 时使用改源码的方式,在 spring-persist-tx.xml 中配置
<!-- 配置 BCryptPasswordEncoder,不在WebAppSecurityConfig中使用@bean的原因
是:在那个类中配置的bean加入到的是SpringMVC的Ioc容器中,而使用时的xxxServiceImpl中
是从Spring的IOC容器中取,因此拿不到bean
-->
<bean id="BCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
</bean>
在 WebAppSecurityConfig 中配置
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
auth
.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder())
;
二、若在加入 SpringSecurity 时使用将 容器 合并的方式,在 WebAppSecurityConfig 配置即可
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
auth
.userDetailsService(userDetailsService)
.passwordEncoder(getBCryptPasswordEncoder())
;
2.6、页面显示用户昵称
显示用户昵称的页面为 include-nav.jsp,以下在该页面进行修改
① 导入SpringSecurity的标签库
<%-- 导入SpringSecurity标签库 --%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
② 通过标签获取登录的用户昵称
<i class="glyphicon glyphicon-user"></i>
<%--${sessionScope.loginAdmin.userName}--%>
<security:authentication property="username"/>
<span class="caret"></span>
<p>SpringSecurity 处理完登录操作之后把登录成功的 User 对象以 principal 属性名存入了 UsernamePasswordAuthenticationToken 对象</p>
Principal:<security:authentication property="principal.class.name"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentication property="principal.originalAdmin.loginAcct"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentication property="principal.originalAdmin.userPswd"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentication property="principal.originalAdmin.userName"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentic ation property="principal.originalAdmin.email"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentication property="principal.originalAdmin.createTime"/><br/>
2.10、SpringSecurity 登陆后密码擦除
擦除密码是在不影响登录认证的情况下,避免密码泄露,增强系统的安全性
本身 SpringSecurity 是会自动把 User 对象中的密码部分擦除。
但是我们创建 SecurityAdmin 对象扩展了 User 对象,User 对象中的密码被擦除了,但是原始 Admin 对象中的密码没有擦除。如果要把原始的 Admin 对象中的密码也擦除需要修改 SecurityAdmin 类代码:
/**
* 构造器
* @param originalAdmin 原始的Admin对象
* @param authorities 该对象的角色、权限信息的集合
*/
public SecurityAdmin(Admin originalAdmin, List<GrantedAuthority> authorities){
// 调用父类构造器
super(originalAdmin.getLoginAcct(), originalAdmin.getUserPswd(), authorities);
// 给本类的originalAdmin赋值
this.originalAdmin = originalAdmin;
// 将原始的Admin对象中的密码擦除
this.originalAdmin.setUserPswd(null);
}
2.11、权限控制
2.11.1、设置测试数据
![](https://img-blog.csdnimg.cn/71979fa8f331479fa6497b1aeb4b1da0.png)
![](https://img-blog.csdnimg.cn/d89d44167241478ba7b1f60c80dbf7f7.png)
![](https://img-blog.csdnimg.cn/04b1427111a8406981a6a58806d7db63.png)
![](https://img-blog.csdnimg.cn/a30b420e1cb4440ca2742c1a3f7173c6.png)
![](https://img-blog.csdnimg.cn/8187c0efbc5142d7b04f150fe6dfd52e.png)
2.11.2、测试一:访问 Admin 分页时具备“经理”角色
在 WebAppSecurityConfig 中的 configure(HttpSecurity http) 方法设置
.antMatchers("/admin/get/page.html") // 访问 Admin 分页功能时要求具备“经理”角色
.hasRole("经理")
2.11.3、测试二:访问 Role 的分页时具备“部长”角色
在 WebAppSecurityConfig 加上 @EnableGlobalMethodSecurity,启动全局方法权限控制
// 启动全局方法权限控制,并且设置 prePostEnabled = true,保证@PreAuthority、@PostAuthority、@PostFilter生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
在 RoleHandle 中的 getPageInfo 方法加上注解 @PreAuthorize
@PreAuthorize("hasRole('部长')")
修改异常处理器 CrowdExceptionResolver 中跳转到 system-error 的方法
/**
* 修改帐号已有的异常映射
* @param exception
* @param request
* @param response
* @return
* @throws IOException
*/
//@ExceptionHandler(value = LoginAcctAlreadyInUseForUpdateException.class)
//public ModelAndView resolvLoginAcctAlreadyInUseForUpdateException(LoginAcctAlreadyInUseForUpdateException exception, HttpServletRequest request, HttpServletResponse response) throws IOException {
// String viewName = "system-error";
// return commonResolve(viewName, exception, request, response);
//}
@ExceptionHandler(value = Exception.class)
public ModelAndView resolveException(Exception exception, HttpServletRequest request, HttpServletResponse response) throws IOException {
String viewName = "system-error";
return commonResolve(viewName, exception, request, response);
}
在 WebAppSecurityConfig 中的 configure(HttpSecurity http) 方法配置异常页面的提示信息
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
request.setAttribute("exception",new Exception(CrowdConstant.MESSAGE_ACCESS_DENIED));
request.getRequestDispatcher("/WEB-INF/system-error.jsp").forward(request, response);
}
})
2.11.4、测试三:要求:访问 Admin 分页功能时具备“经理”角色或“user:get”权限二者之一
添加查询权限给部长操作者角色
![](https://img-blog.csdnimg.cn/12552c2c92084d7197a29c4abba73c91.png)
![](https://img-blog.csdnimg.cn/7315cd3a9a6544aa98e5c31e4d6c05f6.png)
在 WebAppSecurityConfig 中设置
.antMatchers("/admin/get/page.html") // 访问 Admin 分页功能时要求具备“经理”角色
//.hasRole("经理")
.access("hasRole('经理') OR hasAuthority('user:get')")
2.11.5、测试四:访问 Admin 保存功能时具备 user:save 权限
在 AdminHandle 的 save 方法加上注解
@PreAuthorize("hasAuthority('user:save')")
2.12、页面元素的权限控制
页面上的局部元素根据访问控制规则进行控制。
<security:authorize access="hasRole('经理')"></security:authorize>
<security:authorize access="hasAuthority('role:delete')"></security:authorize>