目录
1、加入 Spring-Security 环境
1.1、加入依赖
1.2、在 web.xml 中配置 DelegatingFilterProxy
1.3、创建基于注解的配置类
1.4、谁来把 WebAppSecurityConfig 扫描到 IOC 里?
1.5、找不到 bean 的问题分析
1.5.1、明确三大组件启动顺序
1.5.2、DelegatingFilterProxy 查找 IOC 容器然后查找 bean 的工作机制
1.5.3、解决方法一:改源码
1.5.4、解决方法二:把两个 IOC 容器合二为一
2、目标
2.1、放行登录页和静态资源
2.2、提交登录表单做内存认证
2.3、退出登录
2.4、把内存登录改成数据库登录
2.5、密码加密
2.6、页面显示用户昵称
2.10、SpringSecurity 登陆后密码擦除
2.11、权限控制
2.11.1、设置测试数据
2.11.2、测试一:访问 Admin 分页时具备“经理”角色
2.11.3、测试二:访问 Role 的分页时具备“部长”角色
2.11.4、测试三:要求:访问 Admin 分页功能时具备“经理”角色或“user:get”权限二者之一
2.11.5、测试四:访问 Admin 保存功能时具备 user:save 权限
2.12、页面元素的权限控制
在 atcrowdfunding01-admin-parent 模块和 atcrowdfunding05-common-util 模块的 pom.xml 加入依赖
org.springframework.security
spring-security-web
4.2.10.RELEASE
org.springframework.security
spring-security-config
4.2.10.RELEASE
org.springframework.security
spring-security-taglibs
4.2.10.RELEASE
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
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 {
}
如果是 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 容器。
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
首先:ContextLoaderListener 初始化,创建 Spring 的 IOC 容器
其次:DelegatingFilterProxy 初始化,查找 IOC 容器、查找 bean
最后:DispatcherServlet 初始化,创建 SpringMVC 的 IOC 容器
① 创建与 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);
}
不使用 ContextLoaderListener,让 DispatcherServlet 加载所有 Spring 配置文件。
方法:取消 web.xml 中配置 ContextLoaderListener 并加载所有 spring 的配置文件
缺点:会破坏现有程序的结构。原本是 ContextLoaderListener 和 DispatcherServlet 两个组件创建两个 IOC 容器,现在改成只有一个。
在 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() // 需要认证
;
}
}
① 设置 admin-login.jsp 页面的表单,修改表单提交的地址为 action="security/do/login.html",添加错误信息显示
action="security/do/login.html"
${SPRING_SECURITY_LAST_EXCEPTION.message }
② 将之前在 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")
;
}
① 在 include-nav.jsp 修改退出登录的超链接
② 在 SpringSecurity 的配置类的 configure(HttpSecurity http) 方法设置
.logout() // 开启退出登录功能
.logoutUrl("/security/do/logout.html") // 指定退出登录地址
.logoutSuccessUrl("/admin/to/login/page.html") // 指定退出成功后前往的地址
① 根据 adminId 查询已分配的角色,这个操作在之前已经写好
② 根据 adminId 查询已分配权限
AuthMapper.xml
AuthMapper
/**
* 根据adminId查询已分配的权限
* @param adminId
* @return
*/
List selectAssignedAuthNameByAdminId(Integer adminId);
AuthService
/**
* 根据adminId查询已分配的权限
* @param adminId
* @return
*/
List getAssignedAuthNameByAdminId(Integer adminId);
AuthServiceImpl
/**
* 根据adminId查询已分配的权限
* @param adminId
* @return
*/
@Override
public List 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 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 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 assignedRoleList = roleService.getAssignedRole(adminId);
// 根据adminId查询权限信息
List authNameList = authService.getAssignedAuthNameByAdminId(adminId);
// 存入角色和权限信息
List 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)
;
}
① 修改 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 中配置
在 WebAppSecurityConfig 中配置
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
auth
.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder())
;
二、若在加入 SpringSecurity 时使用将 容器 合并的方式,在 WebAppSecurityConfig 配置即可
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
auth
.userDetailsService(userDetailsService)
.passwordEncoder(getBCryptPasswordEncoder())
;
显示用户昵称的页面为 include-nav.jsp,以下在该页面进行修改
① 导入SpringSecurity的标签库
<%-- 导入SpringSecurity标签库 --%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
② 通过标签获取登录的用户昵称
<%--${sessionScope.loginAdmin.userName}--%>
SpringSecurity 处理完登录操作之后把登录成功的 User 对象以 principal 属性名存入了 UsernamePasswordAuthenticationToken 对象
Principal:
访问 SecurityAdmin 对象的属性:
访问 SecurityAdmin 对象的属性:
访问 SecurityAdmin 对象的属性:
访问 SecurityAdmin 对象的属性:
访问 SecurityAdmin 对象的属性:
擦除密码是在不影响登录认证的情况下,避免密码泄露,增强系统的安全性
本身 SpringSecurity 是会自动把 User 对象中的密码部分擦除。
但是我们创建 SecurityAdmin 对象扩展了 User 对象,User 对象中的密码被擦除了,但是原始 Admin 对象中的密码没有擦除。如果要把原始的 Admin 对象中的密码也擦除需要修改 SecurityAdmin 类代码:
/**
* 构造器
* @param originalAdmin 原始的Admin对象
* @param authorities 该对象的角色、权限信息的集合
*/
public SecurityAdmin(Admin originalAdmin, List authorities){
// 调用父类构造器
super(originalAdmin.getLoginAcct(), originalAdmin.getUserPswd(), authorities);
// 给本类的originalAdmin赋值
this.originalAdmin = originalAdmin;
// 将原始的Admin对象中的密码擦除
this.originalAdmin.setUserPswd(null);
}
在 WebAppSecurityConfig 中的 configure(HttpSecurity http) 方法设置
.antMatchers("/admin/get/page.html") // 访问 Admin 分页功能时要求具备“经理”角色
.hasRole("经理")
在 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);
}
})
添加查询权限给部长操作者角色
在 WebAppSecurityConfig 中设置
.antMatchers("/admin/get/page.html") // 访问 Admin 分页功能时要求具备“经理”角色
//.hasRole("经理")
.access("hasRole('经理') OR hasAuthority('user:get')")
在 AdminHandle 的 save 方法加上注解
@PreAuthorize("hasAuthority('user:save')")
页面上的局部元素根据访问控制规则进行控制。