目标:在原本的项目中使用SpringSecurity,替换原本自己写的登录、访问等机制。
1.加入SpringSecurity环境
1.加入依赖
①在父工程的pom文件规定SpringSecurity的版本信息:
项目结构:
添加依赖:
<!-- SpringSecurity 依赖配置 -->
<!-- ${fall.spring.security.version}:就是4.2.10.RELEASE版本 -->
<!-- SpringSecurity 对 Web 应用进行权限管理 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${fall.spring.security.version}</version>
</dependency>
<!-- SpringSecurity 配置 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${fall.spring.security.version}</version>
</dependency>
<!-- SpringSecurity 标签库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${fall.spring.security.version}</version>
</dependency>
②对SpringSecurity的版本进行统一管理:
项目结构:
添加代码:
<!--通过properties标签指定一些需要重用的版本号,方便在后面调用-->
<properties>
<!-- 声明属性,对SpringSecurity的版本进行统一管理-->
<fall.spring.security.version>4.2.10.RELEASE</fall.spring.security.version>
</properties>
三在component工程中引入依赖:
项目结构:
添加依赖:
<!-- SpringSecurity 对 Web 应用进行权限管理 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<!-- SpringSecurity 配置 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<!-- SpringSecurity 标签库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
</dependency>
2.在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>
3.创建基于注解的SpringSecurity配置类
@Configuration // 设置为配置类
@EnableWebSecurity // 开启web环境下的权限控制功能
// 需要继承WebSecurityConfigurerAdapter
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
4注意
①Spring的IOC容器扫描:
②SpringMVC的IOC容器扫描:
③结论:为了让SpringSecurity能够针对浏览器请求进行权限控制,需要让SpringMVC来扫描WebAppSecurityConfig类。
④衍生问题:DelegatingEilterProxy,初始化时需要到 IOC,容器查找一个 bean,这个 bean 所在的 IOC,容器要看是谁扫描了 WebAppSecurityConfig。
如果是 Spring 扫描了 WebAppSecurityConfig,那么 Filter 需要的 bean 就在
Spring 的 Ioc,容器。
如果是 SpringMVC,扫描了 WebAppSecurityConfig,那么 Filter 需要的 bean
就在 SpringMvc,的 1oc,容器。
⑤明确三大组件启动顺序
首先:ContextLoaderListener初始化,创建 Spring 的 IOC容器。
其次:DelegatingFilterProxy初始化,查找 IOC容器、查找 bean。
最后:DispatcherServlet初始化,创建 SpringMVC.的IOC容器。
⑥Filter查找IOC容器然后查找bean的工作机制
此时启动Tomcat,会==触发找不到springSecurityFilterChain Bean的问题==,这是因为我们创建的WebAppSecurityConfig配置类放在mvc的包下,是交给SpringMVC去扫描的(因为需要让SpringSecurity针对浏览器进行权限控制,就需要让SpringMVC来扫描配置类)。但是DelegatingFilterProxy初始化时,会默认到Spring的容器中寻找springSecurityFilterChain组件,这样是不可能找到的;而在第一次请求时,它依然会去Spring容器中寻找,还是找不到,因此会触发该异常。
⑦解决方案:
一、修改源码,让DelegatingFilterProxy先扫描SpringMVC的容器;
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.web.filter;
import java.io.IOException;
import javax.servlet.*;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.FrameworkServlet;
public class DelegatingFilterProxy extends GenericFilterBean {
private String contextAttribute;
private WebApplicationContext webApplicationContext;
private String targetBeanName;
private boolean targetFilterLifecycle;
private volatile Filter delegate;
private final Object delegateMonitor;
public DelegatingFilterProxy() {
this.targetFilterLifecycle = false;
this.delegateMonitor = new Object();
}
public DelegatingFilterProxy(Filter delegate) {
this.targetFilterLifecycle = false;
this.delegateMonitor = new Object();
Assert.notNull(delegate, "Delegate Filter must not be null");
this.delegate = delegate;
}
public DelegatingFilterProxy(String targetBeanName) {
this(targetBeanName, (WebApplicationContext)null);
}
public DelegatingFilterProxy(String targetBeanName, WebApplicationContext wac) {
this.targetFilterLifecycle = false;
this.delegateMonitor = new Object();
Assert.hasText(targetBeanName, "Target Filter bean name must not be null or empty");
this.setTargetBeanName(targetBeanName);
this.webApplicationContext = wac;
if (wac != null) {
this.setEnvironment(wac.getEnvironment());
}
}
public void setContextAttribute(String contextAttribute) {
this.contextAttribute = contextAttribute;
}
public String getContextAttribute() {
return this.contextAttribute;
}
public void setTargetBeanName(String targetBeanName) {
this.targetBeanName = targetBeanName;
}
protected String getTargetBeanName() {
return this.targetBeanName;
}
public void setTargetFilterLifecycle(boolean targetFilterLifecycle) {
this.targetFilterLifecycle = targetFilterLifecycle;
}
protected boolean isTargetFilterLifecycle() {
return this.targetFilterLifecycle;
}
protected void initFilterBean() throws ServletException {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
if (this.targetBeanName == null) {
this.targetBeanName = this.getFilterName();
}
//改的这里,注释掉即可
// WebApplicationContext wac = this.findWebApplicationContext();
// if (wac != null) {
// this.delegate = this.initDelegate(wac);
// }
}
}
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized(this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
// 把原来查找IOC容器的代码注释掉
// WebApplicationContext wac = this.findWebApplicationContext();
// 按我们的需要查询编写
// 1.获取servletContext对象
ServletContext servletContext = this.getServletContext();
// 2.拼接SpringMVC将IOC容器存入ServletContext域的时候使用的属性名
String servletName = "springDispatcherServlet";
String attrName = FrameworkServlet.SERVLET_CONTEXT_PREFIX + servletName;
// 3.根据attrName从servletContext域中获取IOC容器对象。
WebApplicationContext wac = (WebApplicationContext) servletContext.getAttribute(attrName);
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = this.initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
this.invokeDelegate(delegateToUse, request, response, filterChain);
}
public void destroy() {
Filter delegateToUse = this.delegate;
if (delegateToUse != null) {
this.destroyDelegate(delegateToUse);
}
}
protected WebApplicationContext findWebApplicationContext() {
if (this.webApplicationContext != null) {
if (this.webApplicationContext instanceof ConfigurableApplicationContext) {
ConfigurableApplicationContext cac = (ConfigurableApplicationContext)this.webApplicationContext;
if (!cac.isActive()) {
cac.refresh();
}
}
return this.webApplicationContext;
} else {
String attrName = this.getContextAttribute();
return attrName != null ? WebApplicationContextUtils.getWebApplicationContext(this.getServletContext(), attrName) : WebApplicationContextUtils.findWebApplicationContext(this.getServletContext());
}
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
Filter delegate = (Filter)wac.getBean(this.getTargetBeanName(), Filter.class);
if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig());
}
return delegate;
}
protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
protected void destroyDelegate(Filter delegate) {
if (this.isTargetFilterLifecycle()) {
delegate.destroy();
}
}
}
二、将Spring的IOC容器和SpringMVC的IOC容器在web.xml中合为一个。
这里选用了第二种方法(修改源码的方式相对较复杂,并且修改后,在后面实验过程也需要修改源码,方法二则可以比较快捷)
<!--配置DispatcherServlet(即配置SpringMVC的前端控制器)-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--指定SpringMVC配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-*.xml</param-value>
</init-param>
<!--使DispatcherServlet在Web应用启动时就创建对象并初始化-->
<load-on-startup>1</load-on-startup>
</servlet>
2.在项目中使用SpringSecurity
1.放行登录页与静态资源
1.目标
2.思路
这里需要重写WebSecurityConfigurerAdapter的configure(HttpSecurity security)方法。
3.代码
项目结构:
代码:
@Configuration
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity security) throws Exception {
// String数组,列出需要放行的资源的路径
String[] permitUrls = {"/index.jsp","/bootstrap/**",
"/crowd/**","/css/**","/fonts/**","/img/**",
"/jquery/**","/layer/**","/script/**","/ztree/**","/admin/login/page.html"};
security
.authorizeRequests() // 表示对请求进行授权
.antMatchers(permitUrls) // 传入的ant风格的url
.permitAll() // 允许上面的所有请求,不需要认证
.anyRequest() // 设置其他未设置的全部请求
.authenticated() // 表示需要认证
;
}
}
2.进行登录认证(内存方式)
1.目标
2.思路
3.代码
1.调整登录表单
项目结构:
调整代码:
主要是修改了表单的action、输入框的账号密码的name要与前面的usernameParameter、passwordParameter中的参数相同。
通过${SPRING_SECURITY_LAST_EXCEPTION.message}可以在前端显示由Spring Security抛出的异常信息。
<form action="security/do/login.html" method="post" class="form-signin" role="form">
<h2 class="form-signin-heading"><i class="glyphicon glyphicon-log-in"></i> 用户登录</h2>
<p>${requestScope.exception.message}</p>
<p>${SPRING_SECURITY_LAST_EXCEPTION.message}</p>
<div class="form-group has-success has-feedback">
<input type="text" name="login-user" class="form-control" id="inputSuccess4" placeholder="请输入登录账号" autofocus>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-success has-feedback">
<input type="text" name="login-pwd" class="form-control" id="inputSuccess4" placeholder="请输入登录密码" style="margin-top:10px;">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="checkbox" style="text-align:right;"><a href="reg.html">我要注册</a></div>
<button type="submit" class="btn btn-lg btn-success btn-block">登录</button>
</form>
2.修改WebAppSecurityConfig
项目结构:
修改代码:
依旧是通过configure(HttpSecurity security)方法,在上面的代码的security设置的基础上再加入下面的代码:
@Override
protected void configure(HttpSecurity security) throws Exception {
security.
.csrf() // 设置csrf
.disable() // 关闭csrf
.formLogin() // 开启表单登录功能
.loginPage("/admin/login/page.html") // 指定登陆页面
.usernameParameter("login-user") // 设置表单中对应用户名的标签的name属性名
.passwordParameter("login-pwd") // 设置表单中对应密码的标签的name属性名
.loginProcessingUrl("/security/do/login.html") // 设置登录请求的提交地址
.defaultSuccessUrl("/admin/main/page.html") // 设置登陆成功后前往的地址
.and()
.logout() // 开启退出登录功能
.logoutUrl("/security/do/logout.html") // 设置退出登录的url
.logoutSuccessUrl("/admin/login/page.html") // 设置退出成功后前往的页面
}
这里开启了登录与退出功能后,要修改原先的登录按钮、退出按钮的触发的url,以达到通过SpringSecurity进行登录退出的目的。
关闭csrf是因为这里开发环境为了方便(否则所有提交都需要是post方式,且需要再隐藏域中带csrf的信息,在开发时就比较麻烦)。
3.通过内存的登录认证
此处先演示通过内存的登录认证:
需要重写WebSecurityConfigurerAdapter的configure(AuthenticationManagerBuilder builder)方法。
@Configuration
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder
.inMemoryAuthentication() // 开启在内存中进行身份验证(开发时暂用)
.withUser("tom") // 设置用户名
.password("123456") // 设置密码
.roles("ADMIN"); // 设置权限
}
}
4.把自定义的拦截器进行注释
项目结构:
修改代码:
<!--在mvc容器中注册拦截器 : 在使用SpringSecurity后,就要注释掉原来的自定义的拦截器了=
<mvc:interceptors>
mvc:mapping配置要拦截的资源
【/*】对应一层路径 比如:/aa
【/**】对应多层路径 比如:/aa/bb或者/aa/bb/cc或者/aa/bb/cc/dd
<mvc:interceptor>
<mvc:mapping path="/**"/>
mvc:exclude-mapping配置不要拦截的资源
<mvc:exclude-mapping path="/admin/to/login/page.html"/>
<mvc:exclude-mapping path="/admin/do/logout.html"/>
<mvc:exclude-mapping path="/admin/do/login.html"/>
配置拦截器类
<bean class="com.zh.crowd.mvc.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
-->
3.退出登录
1.修改页面
修改登录后的页面中退出按钮的代码include-nav.jsp:
项目结构:
修改代码:
<ul class="dropdown-menu" role="menu">
<li><a href="#"><i class="glyphicon glyphicon-cog"></i> 个人设置</a></li>
<li><a href="#"><i class="glyphicon glyphicon-comment"></i> 消息</a></li>
<li class="divider"></li>
<li><a href="security/do/logout.html"><i class="glyphicon glyphicon-off"></i> 退出系统</a></li>
</ul>
2.修改WebAppSecurityConfig
依旧是通过configure(HttpSecurity security)方法,在上面的代码的security设置的基础上再加入下面的代码:
.and()
.logout() // 开启退出登录功能
.logoutUrl("/security/do/logout.html") // 设置退出登录的url
.logoutSuccessUrl("/admin/login/page.html") // 设置退出成功后前往的页面
4.进行登录认证(数据库方式)
1.目标
2.思路
3.代码
前提条件:
- 可以通过前端传入的用户名从数据库得到Admin对象
- 可以通过AdminId得到admin对应角色的List
- 可以通过AdminId得到权限的name的List
满足这些条件后,通过实现UserDetailsService接口,通过其loadUserByUsername(String username)方法,传入username,返回最后的结果,也就是所有验则操作交给该实现类来处理。
1.根据adminId查询已经分配的角色
该操作的代码在之前已经实现。
List
public interface RoleService {
public PageInfo<Role> getPageInfo(Integer pageNum,Integer pageSize,String keyword);
void saveRole(Role role);
void updateRole(Role role);
void removeRole(List<Integer> roleIdList);
List<Role> queryUnAssignedRoleList(Integer adminId);
List<Role> queryAssignedRoleList(Integer adminId);
}
2.根据adminId查询已经分配的权限
①AuthService接口
List
public interface AuthService {
List<Auth> queryAuthList();
List<Integer> getAuthByRoleId(Integer roleId);
void saveRoleAuthRelationship(Map<String, List<Integer>> map);
List<String> getAuthNameByAdminId(Integer adminId);
}
②AuthServiceImpl方法
@Override
public List<String> getAuthNameByAdminId(Integer adminId) {
return authMapper.selectAuthNameByAdminId(adminId);
}
③AuthMapper接口
List<String> selectAuthNameByAdminId(Integer adminId);
④AuthMapper.xml代码
通过左外连接查询符合要求的权限名字:
<!-- 通过admin的id得到auth的name -->
<select id="selectAuthNameByAdminId" resultType="string">
SELECT
DISTINCT t_auth.name
from t_auth
LEFT JOIN inner_role_auth ON inner_role_auth.auth_id = t_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>
3.创建一个SecurityAdmin类
项目结构:
代码:
此外,为了方便之后在前端获得Admin更多的信息,创建一个SecurityAdmin类,继承User类,使其在loadUserByUsername方法中返回时,内容更多:
/**
* 为了能方便地获取到原始地Admin对象,因此创建一个SecurityAdmin类,继承User。
*/
public class SecurityAdmin extends User {
private Admin originalAdmin;
public SecurityAdmin(Admin admin, List<GrantedAuthority> authorities){
// 调用父类的构造方法
super(admin.getUserName(),admin.getUserPswd(),authorities);
// 将Admin对象放入对象
this.originalAdmin = admin;
}
public Admin getOriginalAdmin(){
return this.originalAdmin;
}
}
4.根据账号查询Admin
在AdminServiceImpl中添加getAdminByLoginAcct方法(其接口也需要添加该方法):
@Override
public Admin getAdminByLoginAcct(String loginAcct) { // 通过loginAcct得到Admin对象
AdminExample example = new AdminExample();
AdminExample.Criteria criteria = example.createCriteria();
criteria.andLoginAcctEqualTo(loginAcct);
List<Admin> admins = adminMapper.selectByExample(example);
Admin admin = admins.get(0);
return admin;
}
5.创建UserDetailsService实现类
项目结构:
代码:
@Component // 也需要扫描入SpringMVC容器,用于自动注入
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得到角色List
List<Role> roles = roleService.queryAssignedRoleList(admin.getId());
// 通过AdminId得到权限name地List
List<String> authNameList = authService.getAuthNameByAdminId(admin.getId());
// 创建List用来存放GrantedAuthority(权限信息)
List<GrantedAuthority> authorities = new ArrayList<>();
// 向List存放角色信息,注意角色必须要手动加上 “ROLE_” 前缀
for (Role role : roles){
String roleName = "ROLE_" + role.getName();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleName);
authorities.add(simpleGrantedAuthority);
}
// 向List存放权限信息
for (String authName : authNameList){
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authName);
authorities.add(simpleGrantedAuthority);
}
// 将Admin对象、权限信息封装入SecurityAdmin对象(User的子类)
SecurityAdmin securityAdmin = new SecurityAdmin(admin,authorities);
// 返回SecurityAdmin对象
return securityAdmin;
}
}
注意:如果使存入的是角色,这种方式的存入必须要手动加入“ROLE_”前缀。
6.配置类中使用CrowdUserDetailsService
最后在配置类中使用CrowdUserDetailsService:
@Configuration
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder
.userDetailsService(userDetailsService);
}
}
5.密码加密
1.修改t_admin表结构
修改的原因:之前创建的表结构中密码字段的长度不够。
ALTER TABLE t_admin CHANGE user_pswd user_pswd char(100) CHARSET utf8 COLLATE utf8_general_ci NOT NULL ;
2.准备BCryptPasswordEncoder对象
项目结构:
代码:
@Bean
public BCryptPasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
注意:如果在SpringSecurity的配置类中用@Bean注解将BCryptPasswordEncoder对象存入IOC容器,那么Service组件将获取不到。这里不使用这种方式。用下面的方式装配。
因为现在只有一个IOC容器了,因此放在哪个Spring配置文件中都可以,这里是放在spring-persist-tx.xml中
<!-- 将BCryptPasswordEncoder装配入IOC容器 -->
<bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="passwordEncoder"/>
3.使用BCryptPasswordEncoder对象
@Configuration
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Autowired
BCryptPasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder); // 使用BCryptPasswordEncoder进行带盐值的密码加密
}
}
4.使用BCryptPasswordEncoder在新增Admin时加密
修改新增Admin的saveAdmin方法,在AdminServiceImpl中进行修改。
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void saveAdmin(Admin admin) {
// 1.密码加密
String pswd = admin.getUserPswd();
//String md5 = CrowdUtil.md5(pswd);
String encode = passwordEncoder.encode(pswd);
admin.setUserPswd(encode);
6.前端显示登录用户昵称
修改include-nav.jsp的代码:
项目结构:
①引入Spring Security的标签库
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
②使用security标签显示昵称
<security:authentication property="principal.originalAdmin.userName"/>
<%--引入security标签库--%>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<div><a class="navbar-brand" style="font-size:32px;" href="#">众筹平台 - 控制面板</a></div>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li style="padding-top:8px;">
<div class="btn-group">
<button type="button" class="btn btn-default btn-success dropdown-toggle" data-toggle="dropdown">
<i class="glyphicon glyphicon-user">
<%--通过principal.originalAdmin.userName得到当前用户的昵称(principal其实就是前面返回的SecurityAdmin对象)--%>
<security:authentication property="principal.originalAdmin.userName"/>
</i>
<span class="caret"></span>
</button>
property中,principal其实就代表了loadUserByUsername返回的SecurityAdmin对象,因此可以从中取出originalAdmin,得到username。
这也是为什么,需要擦除密码,如果不擦除,那么可以直接从前端获得密码,这样并不安全。
7.密码擦除
对于User对象中自带的密码属性,SpringSecurity已经擦除了.
我们只需要删除SecurityAdmin对象中Admin对象的密码即可:
修改SecurityAdmin的构造方法,最后添加一个设置userPswd=null即可。
public SecurityAdmin(Admin admin, List<GrantedAuthority> authorities){
super(admin.getUserName(),admin.getUserPswd(),authorities);
this.originalAdmin = admin;
// 为了保证安全性,擦除放入originalAdmin的对象的密码
this.originalAdmin.setUserPswd(null);
}
8.权限控制
1.设置测试数据
运行时计算权限的数据:
用户:adminOperator
角色:经理
权限:无
角色:经理操作者
权限:user:save
最终组装后:ROLE_经理,ROLE_经理操作者,user:save
用户:roleOperator
角色:部长
权限:无
角色:部长操作者
权限:role:delete
最终组装后:ROLE_部长,ROLE_部长操作者,role:delete
①设置只有拥有经理角色时,可以访问用户的分页显示页面,只有拥有部长角色时,可以访问角色分页页面
先在前端写好的页面中设置好对应上面的用户的各项数据(角色、权限等)
设置页面的权限:
方法一:通过configure(HttpSecurity security)方法,用HttpSecurity设置:
项目结构:
代码:
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 表示对请求进行授权
.antMatchers(permitUrls) // 传入的ant风格的url
.permitAll() // 允许上面的所有请求,不需要认证
.antMatchers("/admin/page/page.html") // 设置要得到admin的分页信息
.hasRole("经理"); // 必须具有经理的角色
}
也可以不用hasRole这类,而是使用access()方法
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 表示对请求进行授权
.antMatchers(permitUrls) // 传入的ant风格的url
.permitAll() // 允许上面的所有请求,不需要认证
.antMatchers("/admin/page/page.html") // 设置要得到admin的分页信息
.access("hasRole('经理') or hasAuthority('user:get')") // 必须具有经理的角色或有user:get的权限
}
效果:拥有经理角色时,可以访问用户的分页显示页面
adminOperator能够访问:
roleOperator不能访问:
点击用户维护后:
上面的页面为什么没有经过异常处理机制?原因如下:
所以要在SpringSecurity的配置类中进行配置。
给SpringSecurity的权限控制添加异常映射的机制
通过exceptionHandling()方法,以及accessDeniedHandler()传入一个AccessDeniedHandler的匿名实现类:
注意:这种方法,只对security中配置的角色、权限控制有效,在方法上加注解的方式的权限控制,异常会交给前面我们自己编写的异常控制类,因为方法上加注解,抛出异常会被异常控制类捕捉到,但是在configure方法中设置角色、权限信息,则无法被异常控制类捕捉到,需要借助exceptionHandling。
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
throws IOException, ServletException {
request.setAttribute("exception", new Exception("抱歉,您没有权限访问该资源!"));
request.getRequestDispatcher("/WEB-INF/system-error.jsp").forward(request,response);
}
})
}
方法二:在对应的Handler方法上加@PreAuthorize()注解:
// 以json形式显示分页后的role信息
@PreAuthorize("hasRole('部长')")
@ResponseBody
@RequestMapping("/role/page/page.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
PageInfo<Role> pageInfo = roleService.getPageInfo(pageNum, pageSize, keyword);
// 返回ResultEntity,Data就是得到的pageInfo
return ResultEntity.successWithData(pageInfo);
}
注意:如果需要上面的注解生效,需要通过加注解的方法设置,则必须在配置类上加@EnableGlobalMethodSecurity(prePostEnabled = true)注解。
// 启用全局方法权限控制功能,并且设置prePostEnabled = true。
// 保证@PreAuthority、@PostAuthority、@PreFilter、@PostFilter生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration // 设置为配置类
@EnableWebSecurity // 开启web环境下的权限控制功能
// 需要继承WebSecurityConfigurerAdapter
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
效果:
adminOperator不能访问。
roleOperator能够访问。
补充:完善基于注解的异常映射
项目结构:
代码:
@ControllerAdvice
public class CrowdExceptionResolver {
@ExceptionHandler(value = Exception.class)
public ModelAndView resolveException(
Exception exception, HttpServletRequest request,
HttpServletResponse response) throws IOException {
String viewName = "system-error";
return commonCode(exception, request, response, viewName);
}
}
发现:基于注解的异常映射和基于XML的异常映射如果映射同一个异常类型,那么基于注解的方案优先。
hasAuthority()中放权限信息,hasRole()中放角色信息。
而通过access方法,可以让拥有经理角色,或有user:get权限的用户访问用户分页页面。
②访问Admin保存功能时具有user:save权限
此时如果在添加用户的handler方法上加注解,设置只有有user:save权限的用户可以进行新增操作:
@PreAuthorize("hasAuthority('user:save')")
@RequestMapping("/admin/page/doSave.html")
public String addAdmin(Admin admin){
// 调用service层存储admin对象的方法
adminService.saveAdmin(admin);
// 重定向会原本的页面,且为了能在添加管理员后看到管理员,设置pageNum为整型的最大值(通过修正到最后一页)
return "redirect:/admin/page/page.html?pageNum="+Integer.MAX_VALUE;
}
③访问Admin分页功能时具备“经理”角色或者“user:get”权限二者之一
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 表示对请求进行授权
.antMatchers(permitUrls) // 传入的ant风格的url
.permitAll() // 允许上面的所有请求,不需要认证
.antMatchers("/admin/page/page.html") // 设置要得到admin的分页信息
.access("hasRole('经理') or hasAuthority('user:get')") // 必须具有经理的角色或有user:get的权限
}
PS:附带看一下其他注解(了解)
@PostAuthorize:先执行方法然后根据方法返回值判断是否具备权限。
例如:查询一个 Admin 对象,在@PostAuthorize.注解中和当前登录的 Admin 对象
进行比较,如果不一致,则判断为不能访问。实现“只能查自己”效果。
@RostAuthorizel”teturngbiect.data.loginAcct== principal.username”)
使用 returnObject,获取到方法返回值,使用 principal 获取到当前登录用户的主体对象
通过故意写错表达式,然后从异常信息中发现表达式访问的是下面这个类的属性:
org-springframework.security.access.expression.method.MethodSecurityExpressionRoot
@PreFilter:在方法执行前对传入的参数进行过滤。只能对集合类型的数据进行过滤。
@PostFilter:在方法执行后对方法返回值进行规律。只能对集合类型的数据进行过滤。
9.页面元素的权限控制
可以对页面上的局部元素进行访问权限的控制:
需要在JSP页面中引入SpringSecurity的标签库
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
控制如下:
通过security:authorize标签,access与前面的方法一样,在里面写表达式,满足角色、权限的条件则会显示给用户,如果不满足,就不会显示。
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">控制面板</h1>
<div class="row placeholders">
<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>
<security:authorize access="hasAuthority('role:delete')">
<div class="col-xs-6 col-sm-3 placeholder">
<img data-src="holder.js/200x200/auto/vine" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</security:authorize>
... ...
</div>
</div>