文章目錄
歡迎訪問筆者個人技術博客: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使用數據庫認證(知識鋪墊)
- 使用
UserDetails
、UserDetailsService
來完成操作。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 總結
- 導入jar包依賴(2.1)
- 配置web.xml,設置配置文件加載路徑,配置必備監聽器和過濾器(2.2)
- spring-security.xml文件的配置(2.3)
IUserService
繼承UsersDetail
IUserServiceImpl
重寫方法(重要)- 用SpringSecurity提供的
User
增強從數據庫查出來的User
,並利用getAuthority(userInfo.getRoles())
方法獲得SimpleGrantedAuthority
類型的用戶權限集合 - 返回增強後的
user
即可。
- 用SpringSecurity提供的
三、服務器端方法級權限控制
- 在服務器端我們可以通過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
時表示擁有對應的權限method
:method
屬性是配合url
屬性一起使用的,表示用戶應當具有指定url
指定method
訪問的權限,method
的默認值爲GET
,可選值爲http請求的7種方法url
:url
表示如果用戶擁有訪問指定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權限的。其一共定義了三個屬性:hasPermission
、domainObject
和var
,其中前兩個是必須指定的
<security:accesscontrollist hasPermission="" domainObject="" var=""></security:accesscontrollist>
hasPermission
:hasPermission
屬性用於指定以逗號分隔的權限列表domainObject
:domainObject
用於指定對應的域對象var
:var
則是用以將鑑定的結果以指定的屬性名存入pageContext
中,以供同一頁面的其它地方使用