在项目中加入SpringSecurity

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

一、加入SpringSecurity环境

1)、加入依赖

在父工程的pom文件规定SpringSecurity的版本信息:

  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>

在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 {


}

注意:

此时启动Tomcat,会触发找不到springSecurityFilterChain Bean的问题,这是因为我们创建的WebAppSecurityConfig配置类放在mvc的包下,是交给SpringMVC去扫描的(因为需要让SpringSecurity针对浏览器进行权限控制,就需要让SpringMVC来扫描配置类)。但是DelegatingFilterProxy初始化时,会默认到Spring的容器中寻找springSecurityFilterChain组件,这样是不可能找到的;而在第一次请求时,它依然会去Spring容器中寻找,还是找不到,因此会触发该异常。

解决方案:

一、修改源码,让DelegatingFilterProxy先扫描SpringMVC的容器;

二、将Spring的IOC容器和SpringMVC的IOC容器在web.xml中合为一个。

这里选用了第二种方法(修改源码的方式相对较复杂,并且修改后,在后面实验过程也需要修改源码,方法二则可以比较快捷)

6引入SpringSecurity - 图1

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

此时该异常就可以解决了

二、在项目中使用SpringSecurity

1)、放行登录页与静态资源

这里需要重写WebSecurityConfigurerAdapter的configure(HttpSecurity security)方法

@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)、进行登录认证

依旧是通过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的信息,在开发时就比较麻烦)

①修改admin-login.jsp的代码:

主要是修改了表单的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>

②修改登录后的页面中退出按钮的代码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>

此处先演示通过内存的登录认证:

需要重写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");                 // 设置权限
    }
}

测试可用后,替换为通过数据库进行用户登录的认证

前提条件:

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

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

条件1:

在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;
}

条件2:

RoleServiceImpl:此处可以使用前面写好的Service方法,通过adminId得到已经分配的角色的List

@Override
public List<Role> queryAssignedRoleList(Integer adminId) {
    return roleMapper.queryAssignedRoleList(adminId);
}

条件3:

AuthServiceImpl:

@Override
public List<String> getAuthNameByAdminId(Integer adminId) {
    return authMapper.selectAuthNameByAdminId(adminId);
}

AuthMapper.xml:(记得给Mapper接口添加selectAuthNameByAdminId抽象方法,这里省略了)

通过左外连接查询符合要求的权限名字:

<!-- 通过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>

此外,为了方便之后在前端获得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;
    }
}

最后编写UserDetailsService的实现类CrowdUserDetailsService:

@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_“前缀。

最后在配置类中使用CrowdUserDetailsService:

@Configuration
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder
                .userDetailsService(userDetailsService);
    }
}

3)、密码加密与擦除

①密码加密

通过BCryptPasswordEncoder,进行加密

首先将BCryptPasswordEncoder加入IOC容器:

(因为现在只有一个IOC容器了,因此放在哪个Spring配置文件中都可以,这里是放在spring-persist-tx.xml中)

<!-- 将BCryptPasswordEncoder装配入IOC容器 -->
<bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="passwordEncoder"/>

之后在配置类中使用加密:

@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进行带盐值的密码加密
    }
}

②密码擦除

对于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);
}

4)、前端显示登录用户昵称

修改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。

这也是为什么,需要擦除密码,如果不擦除,那么可以直接从前端获得密码,这样并不安全。

5)、权限控制

假设这样一些数据:
用户:adminOperator
角色:经理
权限:无
角色:经理操作者
权限:user:save
最终组装后:ROLE经理,ROLE经理操作者,user:save

用户:roleOperator<br />
    角色:部长<br />
        权限:无<br />
    角色:部长操作者<br />
        权限:role:delete<br />
最终组装后: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的权限
}

方法二:在对应的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) 注解

hasAuthority()中放权限信息,hasRole()中放角色信息。

而通过access方法,可以让拥有经理角色,或有user:get权限的用户访问用户分页页面。

此时如果在添加用户的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;
}

则roleOperator只能访问分页页面,但是不能进行用户增加操作,而adminOperator可以进行用户增加操作。

给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);
            }
        });
}

页面元素的权限控制

可以对页面上的局部元素进行访问权限的控制:

需要在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>