目标:在原本的项目中使用SpringSecurity,替换原本自己写的登录、访问等机制。

1.加入SpringSecurity环境

1.加入依赖

①在父工程的pom文件规定SpringSecurity的版本信息:
项目结构:
image.png
添加依赖:

  1. <!-- SpringSecurity 依赖配置 -->
  2. <!-- ${fall.spring.security.version}:就是4.2.10.RELEASE版本 -->
  3. <!-- SpringSecurity 对 Web 应用进行权限管理 -->
  4. <dependency>
  5. <groupId>org.springframework.security</groupId>
  6. <artifactId>spring-security-web</artifactId>
  7. <version>${fall.spring.security.version}</version>
  8. </dependency>
  9. <!-- SpringSecurity 配置 -->
  10. <dependency>
  11. <groupId>org.springframework.security</groupId>
  12. <artifactId>spring-security-config</artifactId>
  13. <version>${fall.spring.security.version}</version>
  14. </dependency>
  15. <!-- SpringSecurity 标签库 -->
  16. <dependency>
  17. <groupId>org.springframework.security</groupId>
  18. <artifactId>spring-security-taglibs</artifactId>
  19. <version>${fall.spring.security.version}</version>
  20. </dependency>

②对SpringSecurity的版本进行统一管理:
项目结构:
八.项目中加入SpringSecurity - 图2
添加代码:

<!--通过properties标签指定一些需要重用的版本号,方便在后面调用-->
    <properties>
        <!--        声明属性,对SpringSecurity的版本进行统一管理-->
        <fall.spring.security.version>4.2.10.RELEASE</fall.spring.security.version>
    </properties>

三在component工程中引入依赖:
项目结构:
image.png
添加依赖:

<!-- 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中配置

项目结构:
image.png
添加代码:

<!--加入 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容器扫描:
image.png
②SpringMVC的IOC容器扫描:
image.png
③结论:为了让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的工作机制
image.png
此时启动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中合为一个。
这里选用了第二种方法(修改源码的方式相对较复杂,并且修改后,在后面实验过程也需要修改源码,方法二则可以比较快捷)
SpringSecurity的问题.png

<!--配置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.代码

项目结构:
image.png
代码:

@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.目标

利用SpringSecurity通过提交登录表单进行认证。

2.思路

image.png

3.代码

1.调整登录表单

项目结构:
image.png
调整代码:
主要是修改了表单的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

项目结构:
image.png
修改代码:
依旧是通过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.把自定义的拦截器进行注释

项目结构:
image.png
修改代码:

 <!--在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:
项目结构:
image.png
修改代码:

<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.思路

image.png

3.代码

前提条件:

  1. 可以通过前端传入的用户名从数据库得到Admin对象
  2. 可以通过AdminId得到admin对应角色的List
  3. 可以通过AdminId得到权限的name的List

满足这些条件后,通过实现UserDetailsService接口,通过其loadUserByUsername(String username)方法,传入username,返回最后的结果,也就是所有验则操作交给该实现类来处理。

1.根据adminId查询已经分配的角色

该操作的代码在之前已经实现。
List queryUnAssignedRoleList(Integer adminId);

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 getAuthNameByAdminId(Integer adminId);

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类

项目结构:
image.png
代码:
此外,为了方便之后在前端获得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实现类

项目结构:
image.png
代码:

@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对象

项目结构:
image.png
代码:
image.png

@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的代码:
项目结构:
image.png
①引入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已经擦除了.
image.png
我们只需要删除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设置:
项目结构:
image.png
代码:

    @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能够访问:
image.png
roleOperator不能访问:
image.png
点击用户维护后:
image.png
上面的页面为什么没有经过异常处理机制?原因如下:
image.png
所以要在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能够访问。

补充:完善基于注解的异常映射
项目结构:
image.png
代码:

@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>