Java 框架 06 — SpringSecurity(介紹、快速入門、服務器端方法級權限控制、頁面端標籤控制權限)


歡迎訪問筆者個人技術博客:http://rukihuang.xyz/

一、SpringSecurity介紹

  • Spring Security 的前身是 Acegi Security ,是 Spring 項目組中用來提供安全認證服務的框架。

  • 安全包括兩個主要操作:

    • “認證”,是爲用戶建立一個他所聲明的主體。主題一般式指用戶,設備或可以在你係 統中執行動作的其他系統。
    • “授權”指的是一個用戶能否在你的應用中執行某個操作,在到達授權判斷之前,身份的主題已經由 身份驗證過程建立了。

二、快速入門

2.1 pom.xml

  • 在SSM框架中的pom.xml,主要需要導入兩個關鍵的jar包依賴
    • spring-security-web
    • spring-security-config
<dependencies>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>5.0.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>5.0.1.RELEASE</version>
    </dependency>
</dependencies>

2.2 web.xml

  • 設置配置文件的加載路徑
  • 配置監聽器ContextLoaderListener
  • 配置過濾器DelegatingFilterProxy
<context-param>
    <param-name>contextConfigLocation</param-name>
    <!--需要讀取類路徑下的spring-security.xml文件-->
    <param-value>classpath:spring-security.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

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

2.3 spring-security.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd">

    <!--開啓註解配置-->
    <!--<security:global-method-security jsr250-annotations="enabled"/>-->
    <security:global-method-security pre-post-annotations="enabled" jsr250-annotations="enabled" secured-annotations="enabled"/>

    <!-- 配置不攔截的資源 -->
    <security:http pattern="/login.jsp" security="none"/>
    <security:http pattern="/failer.jsp" security="none"/>
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/plugins/**" security="none"/>

    <!--
    	配置具體的規則
    	auto-config="true"	不用自己編寫登錄的頁面,框架提供默認登錄頁面
    	use-expressions="false"	是否使用SPEL表達式(沒學習過)
    -->
    <security:http auto-config="true" use-expressions="true">
        <!-- 配置具體的攔截的規則 pattern="請求路徑的規則" access="訪問系統的人,必須有ROLE_USER的角色" -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')"/>

        <!-- 定義跳轉的具體的頁面 -->
        <security:form-login
                login-page="/login.jsp"
                login-processing-url="/login.do"
                default-target-url="/index.jsp"
                authentication-failure-url="/failer.jsp"
                authentication-success-forward-url="/pages/main.jsp"
        />

        <!-- 關閉跨域請求 -->
        <security:csrf disabled="true"/>

        <!-- 退出 -->
        <security:logout invalidate-session="true" logout-url="/logout.do" logout-success-url="/login.jsp"/>
    </security:http>

    <!-- 切換成數據庫中的用戶名和密碼 -->
    <!--authentication-manager:認證管理器-->
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userService">
            <!-- 配置加密的方式
 				一旦配置,代碼部分就不能以明文顯示,即"{noop}"+password失效-->
            <security:password-encoder ref="passwordEncoder"/>
        </security:authentication-provider>
    </security:authentication-manager>

    <!-- 配置加密類 -->
    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

    <!-- 提供了入門的方式,在內存中存入用戶名和密碼
    <security:authentication-manager>
    	<security:authentication-provider>
    		<security:user-service>
    			<security:user name="admin" password="{noop}admin" authorities="ROLE_USER"/>
    		</security:user-service>
    	</security:authentication-provider>
    </security:authentication-manager>
    -->

</beans>

2.4.0 SpringSecurity使用數據庫認證(知識鋪墊)

  • 使用UserDetailsUserDetailsService來完成操作。SpringSecurity提供了一個UserDetails的實現類User來完成操作。
  • UserDetails
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}
  • User
public class User implements UserDetails, CredentialsContainer {
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired; //帳戶是否過期
    private final boolean accountNonLocked; //帳戶是否鎖定
    private final boolean credentialsNonExpired; //認證是否過期
    private final boolean enabled; //帳戶是否可用
    
        public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }

    public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        if (username != null && !"".equals(username) && password != null) {
            this.username = username;
            this.password = password;
            this.enabled = enabled;
            this.accountNonExpired = accountNonExpired;
            this.credentialsNonExpired = credentialsNonExpired;
            this.accountNonLocked = accountNonLocked;
            this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
        } else {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }
    }
  • UserDetailsService
public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

2.4 用戶登陸

2.4.1 IUserService

  • 繼承UserDetailsService
package com.ruki.eams.service;

import com.ruki.eams.domain.Role;
import com.ruki.eams.domain.UserInfo;
import org.springframework.security.core.userdetails.UserDetailsService;

import java.util.List;

public interface IUserService extends UserDetailsService {

}

2.4.2 UserServiceImpl

  • 重寫方法
package com.ruki.eams.service.impl;

import com.ruki.eams.dao.IUserDao;
import com.ruki.eams.domain.Role;
import com.ruki.eams.domain.UserInfo;
import com.ruki.eams.service.IUserService;
import com.sun.org.apache.bcel.internal.generic.RETURN;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service("userService")
@Transactional
public class UserServiceImpl implements IUserService {

    @Autowired
    private IUserDao userDao;
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = null;
        try {
            userInfo = userDao.findByUsername(username);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //將包含用戶信息的userInfo對象封裝成UserDetails,明文密碼需要在密碼前加上前綴
        //User user = new User(userInfo.getUsername(), "{noop}"+userInfo.getPassword(), getAuthority(userInfo.getRoles()));

        //配置文件中<security:password-encoder ref="passwordEncoder"/>要註釋掉
        //User user = new User(userInfo.getUsername(), "{noop}"+userInfo.getPassword(), userInfo.getStatus() == 1?true:false,true,true,true,getAuthority(userInfo.getRoles()));

		//如果有賬戶狀態的信息,需要傳入狀態信息參數
        //public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities)
        User user = new User(userInfo.getUsername(), userInfo.getPassword(), userInfo.getStatus() == 1?true:false,true,true,true,getAuthority(userInfo.getRoles()));
        return user;
    }

    //獲得用戶權限集合的方法,集合中裝的是角色描述
    public List<SimpleGrantedAuthority> getAuthority(List<Role> roles){
        List<SimpleGrantedAuthority> list = new ArrayList<>();
        for(Role role : roles){
            list.add(new SimpleGrantedAuthority("ROLE_"+role.getRoleName()));
        }

        return list;
    }

    @Override
    public void save(UserInfo userInfo) throws Exception {
        //對密碼進行加密
        userInfo.setPassword(bCryptPasswordEncoder.encode(userInfo.getPassword()));
        userDao.save(userInfo);
    }

}

2.4.3 IUserDao

package com.ruki.eams.dao;

import com.ruki.eams.domain.Role;
import com.ruki.eams.domain.UserInfo;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface IUserDao {

    @Select("select *from users where username=#{username}")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "email", column = "email"),
            @Result(property = "username", column = "username"),
            @Result(property = "password", column = "password"),
            @Result(property = "phoneNum", column = "phoneNum"),
            @Result(property = "status", column = "status"),
            @Result(property = "roles", column = "id", javaType = java.util.List.class, many = @Many(select = "com.ruki.eams.dao.IRoleDao.findRoleByUserId"))
    })
    UserInfo findByUsername(String username) throws Exception;

    @Select("select * from users where id=#{id}")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "email", column = "email"),
            @Result(property = "username", column = "username"),
            @Result(property = "password", column = "password"),
            @Result(property = "phoneNum", column = "phoneNum"),
            @Result(property = "status", column = "status"),
            @Result(property = "roles", column = "id", javaType = java.util.List.class, many = @Many(select = "com.ruki.eams.dao.IRoleDao.findRoleByUserId"))
    })
    UserInfo findById(String id);
}

2.5 用戶退出

  • 在spring-security.xml文件中添加配置
<security:logout invalidate-session="true" logout-url="/logout.do" logout-successurl="/login.jsp" />

2.6 總結

  1. 導入jar包依賴(2.1)
  2. 配置web.xml,設置配置文件加載路徑,配置必備監聽器和過濾器(2.2)
  3. spring-security.xml文件的配置(2.3)
  4. IUserService繼承UsersDetail
  5. IUserServiceImpl重寫方法(重要
    1. 用SpringSecurity提供的User增強從數據庫查出來的User,並利用getAuthority(userInfo.getRoles())方法獲得SimpleGrantedAuthority類型的用戶權限集合
    2. 返回增強後的user即可。

三、服務器端方法級權限控制

  • 在服務器端我們可以通過Spring security提供的註解對方法來進行權限控制。Spring Security在方法的權限控制上支持三種類型的註解,JSR-250註解、@Secured註解和支持表達式的註解,這三種註解默認都是沒有啓用的,需要單獨通過global-method-security元素的對應屬性進行啓用

3.1 開啓註解使用

  • 配置文件

    <security:global-method-security jsr250-annotations="enabled"/>
    <security:global-method-security secured-annotations="enabled"/>
    <security:global-method-security pre-post-annotations="disabled"/>
    

3.2 JSR-250註解

  • @RolesAllowed:訪問對應方法時鎖應該具有的角色
  • @PermitAll:允許所以的角色急性訪問,即不進行權限控制
  • @DenyAll:拒絕所有角色的訪問
@Controller
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private IProductService productService;

    @RequestMapping("/findAll.do")
    @RolesAllowed("ADMIN")//表示只有ADMIN用戶才能使用,JSR250前綴ROLE_可以省略
    public ModelAndView findAll(@RequestParam(name = "page", required = true, defaultValue = "1") Integer page, @RequestParam(name="size", required = true, defaultValue = "4") Integer size) throws Exception {
        ModelAndView mv = new ModelAndView();
        List<Product> products = productService.findAll(page, size);
        PageInfo pageInfo = new PageInfo(products);

        mv.addObject("pageInfo", pageInfo);
        mv.setViewName("product-page-list1");
        return mv;
    }

}

3.3 支持表達式的註解

  • @PreAuthorize 在方法調用之前,基於表達式的計算結果來限制對方法的訪問
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
void changePassword(@P("userId") long userId ){ }
//這裏表示在changePassword方法執行之前,判斷方法參數userId的值是否等於principal中保存的當前用戶的userId,或者當前用戶是否具有ROLE_ADMIN權限,兩種符合其一,就可以訪問該方法。
  • @PostAuthorize 允許方法調用,但是如果表達式計算結果爲false,將拋出一個安全性異常

  • @PostFilter 允許方法調用,但必須按照表達式來過濾方法的結果

  • @PreFilter 允許方法調用,但必須在進入方法之前過濾輸入值

3.4 @Secured註解

  • @Secured註解標註的方法進行權限控制的支持,其值默認爲disabled
@Controller
@RequestMapping("/orders")
public class OrdersController {
    @Autowired
    private IOrdersService ordersService;

    @RequestMapping("/findAll.do")
    @Secured("ROLE_ADMIN")// @Secured註解下,前綴不能省略
    public ModelAndView findAll(@RequestParam(name = "page", required = true, defaultValue = "1") Integer page, @RequestParam(name = "size", required = true, defaultValue = "4") Integer size) throws Exception {
        ModelAndView mv = new ModelAndView();
        List<Orders> ordersList = ordersService.findAll(page, size);
        //PageInfo就是一個分頁Bean
        PageInfo pageInfo = new PageInfo(ordersList);
        mv.addObject("pageInfo", pageInfo);
        mv.setViewName("orders-page-list");
        return mv;
    }
}

四、頁面端標籤控制權限

  • 在jsp頁面中我們可以使用spring security提供的權限標籤來進行權限控制

4.1 導入依賴

4.1.1 maven導入

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>version</version>
</dependency>

4.1.2 頁面導入

<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%>

4.2 常用標籤

4.2.1 authentication

<security:authentication property="" htmlEscape="" scope="" var=""/>
  • property: 只允許指定Authentication所擁有的屬性,可以進行屬性的級聯獲取,如“principle.username”,不允許直接通過方法進行調用
  • htmlEscape:表示是否需要將html進行轉義。默認爲true
  • scope:與var屬性一起使用,用於指定存放獲取的結果的屬性名的作用範圍,默認爲pageContext。Jsp中擁有的作用範圍都進行進行指定
  • var: 用於指定一個屬性名,這樣當獲取到了authentication的相關信息後會將其以var指定的屬性名進行存放,默認是存放在pageConext

4.2.2 authorize

  • authorize是用來判斷普通權限的,通過判斷用戶是否具有對應的權限而控制其所包含內容的顯示
<security:authorize access="" method="" url="" var=""></security:authorize>
  • access: 需要使用表達式來判斷權限,當表達式的返回結果爲true時表示擁有對應的權限
  • methodmethod屬性是配合url屬性一起使用的,表示用戶應當具有指定url指定method訪問的權限,method的默認值爲GET,可選值爲http請求的7種方法
  • urlurl表示如果用戶擁有訪問指定url的權限即表示可以顯示authorize標籤包含的內容
  • var:用於指定將權限鑑定的結果存放在pageContext的哪個屬性中
<security:authorize access="hasRole('ADMIN')"><%-- ADMIN角色才能看得到--%>
	<a href="${pageContext.request.contextPath}/user/findAll.do"> 
		<i class="fa fa-circle-o"></i> 
		用戶管理
	</a>
</security:authorize>

4.2.3 accesscontrollist

  • accesscontrollist標籤是用於鑑定ACL權限的。其一共定義了三個屬性:hasPermissiondomainObjectvar,其中前兩個是必須指定的
<security:accesscontrollist hasPermission="" domainObject="" var=""></security:accesscontrollist>
  • hasPermissionhasPermission屬性用於指定以逗號分隔的權限列表
  • domainObjectdomainObject用於指定對應的域對象
  • varvar則是用以將鑑定的結果以指定的屬性名存入pageContext中,以供同一頁面的其它地方使用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章