Spring Security 簡(jiǎn)介
Spring Security 主要作用是認(rèn)證與授權(quán)
Spring Security 和 jwt 相關(guān)的介紹自行百度吧
下面直接上代碼,注意看注釋 有相關(guān)的代碼作用解釋赛糟,如果有錯(cuò)誤的地方,請(qǐng)指出 一起學(xué)習(xí) 謝謝客给!
認(rèn)證大概流程
結(jié)構(gòu)目錄
pom.xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<security-jwt.version>1.0.9.RELEASE</security-jwt.version>
<jjwt.version>0.9.0</jjwt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${security-jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 18081
spring:
application:
name: oauth-server # 應(yīng)用名稱
jpa:
open-in-view: true
database: POSTGRESQL
show-sql: true
hibernate:
ddl-auto: update
dialect: org.hibernate.dialect.PostgreSQLDialect
properties:
hibernate:
temp:
use_jdbc_metadata_defaults: false
# 數(shù)據(jù)源 配置
datasource:
platform: postgres
url: jdbc:postgresql://127.0.0.1:5432/cloud_oauth2?useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres123
driver-class-name: org.postgresql.Driver
# redis 配置
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
jedis:
pool:
#最大連接數(shù)
max-active: 8
#最大空閑
max-idle: 8
#最大阻塞等待時(shí)間(負(fù)數(shù)表示沒(méi)限制)
max-wait: -1ms
#最小空閑
min-idle: 0
#連接超時(shí)時(shí)間
timeout: 1000ms
# JWT 配置
jwt:
# 存放Token的Header Key
header: Authorization
# 密匙key
secret: mySecret
# 過(guò)期時(shí)間 單位秒 7天后過(guò)期 604800
expiration: 3600
# 自定義token 前綴字符
tokenHead: Bearer-
# 超時(shí)時(shí)間 單位秒
access_token: 3600
# 刷新token時(shí)間 單位秒
refresh_token: 3600
route:
authentication:
path: login/entry
refresh: oauth/refresh
register: login/account
# 配置不需要認(rèn)證的接口
com:
example:
oauth:
security:
antMatchers:
/auth/v1/api/login/**,
/auth/v1/api/module/tree/**,
/auth/v1/api/grid/**
# 日志
logging:
level:
org:
springframework:
security: DEBUG
必須的配置類
- web安全配置類 WebSecurityConfig
- 用戶身份權(quán)限認(rèn)證類 MyUserDetailService
- 資源權(quán)限認(rèn)證器 MyAccessDecisionManager
- 請(qǐng)求過(guò)濾類 MyFilterSecurityInterceptor
- 加載資源與權(quán)限的關(guān)系 MyInvocationSecurityMetadataSourceService
可選的處理類
- 權(quán)限不足處理類 MyAccessDeniedHandler
- 異常處理類 MyAuthenticationException
- 登錄成功后處理類 MyAuthenticationSuccessHandler
- 登錄失敗后處理類 MyAuthenticationFailureHandler
- 退出系統(tǒng)成功后處理類 MyLogoutSuccessHandler
- 登錄驗(yàn)證(比如校驗(yàn)驗(yàn)證碼) MyUsernamePasswordAuthenticationFilter
- 認(rèn)證失敗處理類 MyAuthenticationEntryPointHandler
整合jwt 需要的類
- jwt 工具類 提供校驗(yàn)toeken 蓖宦、生成token、根據(jù)token獲取用戶等方法 JwtTokenUtil
- 用戶信息 JWTUserDetails
- JWTUserDetailsFactory
- 對(duì)請(qǐng)求的token進(jìn)行校驗(yàn) JwtAuthenticationTokenFilter
MyAccessDecisionManager 資源權(quán)限認(rèn)證器 認(rèn)證用戶是否擁有所請(qǐng)求資源的權(quán)限
/***
*
* @FileName: MyAccessDecisionManager
* @Company:
* @author
* @Date 2018年05月11日
* @version 1.0.0
* @remark: 資源權(quán)限認(rèn)證器 證用戶是否擁有所請(qǐng)求資源的權(quán)限
* @explain 接口AccessDecisionManager也是必須實(shí)現(xiàn)的诸老。 decide方法里面寫(xiě)的就是授權(quán)策略了界睁,需要什么策略觉增,可以自己寫(xiě)其中的策略邏輯
* 認(rèn)證通過(guò)就返回,不通過(guò)拋異常就行了翻斟,spring security會(huì)自動(dòng)跳到權(quán)限不足處理類(WebSecurityConfig 類中 配置文件上配的)
*
*
*/
@Slf4j
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
/**
* 授權(quán)策略
*
* decide()方法在url請(qǐng)求時(shí)才會(huì)調(diào)用逾礁,服務(wù)器啟動(dòng)時(shí)不會(huì)執(zhí)行這個(gè)方法
*
* @param configAttributes 裝載了請(qǐng)求的url允許的角色數(shù)組 。這里是從MyInvocationSecurityMetadataSource里的loadResourceDefine方法里的atts對(duì)象取出的角色數(shù)據(jù)賦予給了configAttributes對(duì)象
* @param object url
* @param authentication 裝載了從數(shù)據(jù)庫(kù)讀出來(lái)的權(quán)限(角色) 數(shù)據(jù)。這里是從MyUserDetailService里的loadUserByUsername方法里的grantedAuths對(duì)象的值傳過(guò)來(lái)給 authentication 對(duì)象,簡(jiǎn)單點(diǎn)就是從spring的全局緩存SecurityContextHolder中拿到的嘹履,里面是用戶的權(quán)限信息
*
* 注意: Authentication authentication 如果是前后端分離 則有跨域問(wèn)題腻扇,跨域情況下 authentication 無(wú)法獲取當(dāng)前登陸人的身份認(rèn)證(登陸成功后),我嘗試用token來(lái)效驗(yàn)權(quán)限
*
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// 無(wú)權(quán)限訪問(wèn)
if(CollectionUtils.isEmpty(configAttributes)){
log.info("無(wú)訪問(wèn)權(quán)限.");
throw new AccessDeniedException("無(wú)訪問(wèn)權(quán)限.");
}
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()){
ConfigAttribute configAttribute = iterator.next();
String needRole = configAttribute.getAttribute();
for(GrantedAuthority grantedAuthority : authentication.getAuthorities()){
//grantedAuthority 為用戶所被賦予的權(quán)限砾嫉。 needRole 為訪問(wèn)相應(yīng)的資源應(yīng)該具有的權(quán)限幼苛。
//判斷兩個(gè)請(qǐng)求的url的權(quán)限和用戶具有的權(quán)限是否相同,如相同焕刮,允許訪問(wèn) 權(quán)限就是那些以ROLE_為前綴的角色
if (needRole.trim().equals(grantedAuthority.getAuthority().trim())){
//匹配到對(duì)應(yīng)的角色舶沿,則允許通過(guò)
return;
}
}
}
//該url具有訪問(wèn)權(quán)限,但是當(dāng)前登錄用戶沒(méi)有匹配到URL對(duì)應(yīng)的權(quán)限配并,則拋出無(wú)權(quán)限錯(cuò)誤
log.info("無(wú)訪問(wèn)權(quán)限.");
throw new AccessDeniedException("無(wú)訪問(wèn)權(quán)限.");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
MyInvocationSecurityMetadataSourceService 加載資源與權(quán)限的對(duì)應(yīng)關(guān)系
/***
*
* @FileName: MyInvocationSecurityMetadataSourceService
* @Company:
* @author
* @Date 2018年05月11日
* @version 1.0.0
* @remark: 加載資源與權(quán)限的對(duì)應(yīng)關(guān)系
* @explain 實(shí)現(xiàn)FilterInvocationSecurityMetadataSource接口也是必須的括荡。 首先,這里從數(shù)據(jù)庫(kù)中獲取信息溉旋。 其中l(wèi)oadResourceDefine方法不是必須的畸冲,
* 這個(gè)只是加載所有的資源與權(quán)限的對(duì)應(yīng)關(guān)系并緩存起來(lái),避免每次獲取權(quán)限都訪問(wèn)數(shù)據(jù)庫(kù)(提高性能)观腊,然后getAttributes根據(jù)參數(shù)(被攔截url)返回權(quán)限集合邑闲。
* 這種緩存的實(shí)現(xiàn)其實(shí)有一個(gè)缺點(diǎn),因?yàn)閘oadResourceDefine方法是放在構(gòu)造器上調(diào)用的梧油,而這個(gè)類的實(shí)例化只在web服務(wù)器啟動(dòng)時(shí)調(diào)用一次苫耸,那就是說(shuō)loadResourceDefine方法只會(huì)調(diào)用一次,
* 如果資源和權(quán)限的對(duì)應(yīng)關(guān)系在啟動(dòng)后發(fā)生了改變儡陨,那么緩存起來(lái)的權(quán)限數(shù)據(jù)就和實(shí)際授權(quán)數(shù)據(jù)不一致鲸阔,那就會(huì)授權(quán)錯(cuò)誤了。但如果資源和權(quán)限對(duì)應(yīng)關(guān)系是不會(huì)改變的迄委,這種方法性能會(huì)好很多。
* 要想解決 權(quán)限數(shù)據(jù)的一致性 可以直接在getAttributes方法里面調(diào)用數(shù)據(jù)庫(kù)操作獲取權(quán)限數(shù)據(jù)类少,通過(guò)被攔截url獲取數(shù)據(jù)庫(kù)中的所有權(quán)限叙身,封裝成Collection<ConfigAttribute>返回就行了。(靈活硫狞、簡(jiǎn)單
*
* 器啟動(dòng)加載順序:1:調(diào)用loadResourceDefine()方法 2:調(diào)用supports()方法 3:調(diào)用getAllConfigAttributes()方法
*
*
*/
@Slf4j
@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
//存放資源配置對(duì)象
private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
@Autowired
private ModuleService moduleService;
@Autowired
private RoleService roleService;
@Autowired
private UrlMatcher urlMatcher;
/**
* 參數(shù)是要訪問(wèn)的url信轿,返回這個(gè)url對(duì)于的所有權(quán)限(或角色)
* 每次請(qǐng)求后臺(tái)就會(huì)調(diào)用 得到請(qǐng)求所擁有的權(quán)限
* 這個(gè)方法在url請(qǐng)求時(shí)才會(huì)調(diào)用,服務(wù)器啟動(dòng)時(shí)不會(huì)執(zhí)行這個(gè)方法
* getAttributes這個(gè)方法會(huì)根據(jù)你的請(qǐng)求路徑去獲取這個(gè)路徑應(yīng)該是有哪些權(quán)限才可以去訪問(wèn)残吩。
*
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// if (resourceMap == null){ //取消這段代碼注釋 情況下 每次服務(wù)啟動(dòng)后請(qǐng)求后臺(tái)只有到數(shù)據(jù)庫(kù)中取一次權(quán)限 如果注釋掉這段代碼則每次請(qǐng)求都會(huì)到數(shù)據(jù)庫(kù)中取權(quán)限
loadResourceDefine(); // 每次請(qǐng)求 都會(huì)去數(shù)據(jù)庫(kù)查詢權(quán)限 貌似很耗性能
// }
// object 是一個(gè)URL财忽,被用戶請(qǐng)求的url。
String url = ((FilterInvocation) object).getRequestUrl();
log.info("請(qǐng)求 url :" + url);
int firstQuestionMarkIndex = url.indexOf("?");
if (firstQuestionMarkIndex != -1) {
url = url.substring(0, firstQuestionMarkIndex);
}
//循環(huán)已有的角色配置對(duì)象 進(jìn)行url匹配
Iterator<String> ite = resourceMap.keySet().iterator();
while (ite.hasNext()) {
String resURL = ite.next().trim();
if (urlMatcher.pathMatchesUrl(resURL, url)) { // 路徑支持Ant風(fēng)格的通配符 /spitters/**
return resourceMap.get(resURL);
}
/* if (url.equals(resURL)) { // 路徑不支持Ant風(fēng)格的通配符
//返回當(dāng)前 url 所需要的權(quán)限
return resourceMap.get(resURL);
}*/
}
return null ;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
//要返回true 不然要報(bào)異称辏 SecurityMetadataSource does not support secure object class: class
return true;
}
/**
* 初始化資源 ,提取系統(tǒng)中的所有權(quán)限即彪,加載所有url和權(quán)限(或角色)的對(duì)應(yīng)關(guān)系, web容器啟動(dòng)就會(huì)執(zhí)行
* 如果啟動(dòng)@PostConstruct 注解 則web容器啟動(dòng)就會(huì)執(zhí)行
*/
//@PostConstruct
public void loadResourceDefine() {
// if (resourceMap == null) {
//應(yīng)當(dāng)是資源為key活尊, 權(quán)限為value隶校。 資源通常為url漏益, 權(quán)限就是那些以ROLE_為前綴的角色。 一個(gè)資源可以由多個(gè)權(quán)限來(lái)訪問(wèn)深胳。
resourceMap = new ConcurrentHashMap<>();
//獲取所有分配的角色
List<SysRole> roleList = this.roleService.findByRoleModule();
//容器啟動(dòng)時(shí),獲取全部系統(tǒng)菜單資源信息
List<SysModuleVO> moduleList = this.moduleService.findByRoleModule();
if (!CollectionUtils.isEmpty(roleList)){
for (SysRole role : roleList){
//授權(quán)標(biāo)識(shí)
String authorizedSigns = role.getAuthorizedSigns().trim();
ConfigAttribute configAttributes = new SecurityConfig(authorizedSigns);
for (SysModuleVO module : moduleList){
boolean flag = String.valueOf(role.getId()).equals(module.getAuthorizedSigns());
if(flag){
//請(qǐng)求url
String url =StringUtils.isNotBlank(module.getMenuUrl()) ? module.getMenuUrl().trim() : "";
// 判斷資源文件和權(quán)限的對(duì)應(yīng)關(guān)系绰疤,如果已經(jīng)存在相關(guān)的資源url,則要通過(guò)該url為key提取出權(quán)限集合舞终,將權(quán)限增加到權(quán)限集合中轻庆。
if (resourceMap.containsKey(url)) {
Collection<ConfigAttribute> value = resourceMap.get(url);
value.add(configAttributes);
resourceMap.put(url, value);
} else {
Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>();
atts.add(configAttributes);
resourceMap.put(url, atts);
}
}
}
}
}
// }
}
}
MyFilterSecurityInterceptor 請(qǐng)求過(guò)濾
/***
*
* @FileName: MyFilterSecurityInterceptor
* @Company:
* @author
* @Date 2018年05月11日
* @version 1.0.0
* @remark: 過(guò)濾用戶請(qǐng)求
* @explain 繼承AbstractSecurityInterceptor、實(shí)現(xiàn)Filter是必須的
* 首先敛劝,登陸后余爆,每次訪問(wèn)資源都會(huì)被這個(gè)攔截器攔截,會(huì)執(zhí)行doFilter這個(gè)方法攘蔽,這個(gè)方法調(diào)用了invoke方法龙屉,其中fi斷點(diǎn)顯示是一個(gè)url
* 最重要的是beforeInvocation這個(gè)方法,它首先會(huì)調(diào)用MyInvocationSecurityMetadataSource類的getAttributes方法獲取被攔截url所需的權(quán)限
* 在調(diào)用MyAccessDecisionManager類decide方法判斷用戶是否具有權(quán)限,執(zhí)行完后就會(huì)執(zhí)行下一個(gè)攔截器
*
*
*/
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
/**
* 登錄后 每次請(qǐng)求都會(huì)調(diào)用這個(gè)攔截器進(jìn)行請(qǐng)求過(guò)濾
* @param servletRequest
* @param servletResponse
* @param filterChain
* @throws IOException
* @throws ServletException
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
/* @Override
public void setAccessDecisionManager(MyAccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(this.accessDecisionManager);
}*/
@Autowired
public void setAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
/**
* 攔截請(qǐng)求處理
* @param fi
* @throws IOException
* @throws ServletException
*/
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一個(gè)被攔截的url
//里面調(diào)用MyInvocationSecurityMetadataSource的getAttributes(Object object)這個(gè)方法獲取fi對(duì)應(yīng)的所有權(quán)限
//再調(diào)用MyAccessDecisionManager的decide方法來(lái)校驗(yàn)用戶的權(quán)限是否足夠
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//執(zhí)行下一個(gè)攔截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
}
MyUserDetailService 用戶登錄身份認(rèn)證
/***
*
* @FileName: MyUserDetailService
* @Company:
* @author
* @Date 2018年05月11日
* @version 1.0.0
* @remark: 配置用戶權(quán)限認(rèn)證
* @explain 當(dāng)用戶登錄時(shí)會(huì)進(jìn)入此類的loadUserByUsername方法對(duì)用戶進(jìn)行驗(yàn)證满俗,驗(yàn)證成功后會(huì)被保存在當(dāng)前回話的principal對(duì)象中
* 系統(tǒng)獲取當(dāng)前登錄對(duì)象信息方法 WebUserDetails webUserDetails = (WebUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
*
* 異常信息:
* UsernameNotFoundException 用戶找不到
* BadCredentialsException 壞的憑據(jù)
* AccountExpiredException 賬戶過(guò)期
* LockedException 賬戶鎖定
* DisabledException 賬戶不可用
* CredentialsExpiredException 證書(shū)過(guò)期
*
*
*/
@Slf4j
@Service("myUserDetailService")
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserRoleService userRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("登錄用戶:" + username);
//用戶用戶信息和用戶角色
UserRoleVO userRole = this.userRoleService.findUserAndRole(username);
if(userRole.getUserId() == null){
//后臺(tái)拋出的異常是:org.springframework.security.authentication.BadCredentialsException: Bad credentials 壞的憑證 如果要拋出UsernameNotFoundException 用戶找不到異常則需要自定義重新它的異常
log.info("登錄用戶:" + username + " 不存在.");
throw new UsernameNotFoundException("登錄用戶:" + username + " 不存在");
}
//獲取用戶信息
UserInfoVO userInfo = userRole.getUserInfo();
//獲取用戶擁有的角色
List<RoleVO> roleList = userRole.getRoles();
Set<GrantedAuthority> grantedAuths = new HashSet<GrantedAuthority>();
if(roleList.size() > 0){
roleList.stream().forEach(role ->{
grantedAuths.add(new SimpleGrantedAuthority(role.getAuthorizedSigns()));
});
}
User userDetail = new User(userInfo.getUserAccount(),userInfo.getUserPwd(),
grantedAuths);
//不使用jwt 代碼
//return userDetail;
//使用JWT 代碼
UserDetail user = DozerBeanMapperUtil.copyProperties(userInfo,UserDetail.class);
user.setUserId(userInfo.getId());
return JWTUserDetailsFactory.create(userDetail,user);
}
}
WebSecurityConfig web 安全配置
/***
*
* @FileName: WebSecurityConfig
* @Company:
* @author ljy
* @Date 2018年05月11日
* @version 1.0.0
* @remark: web 安全性配置
* @explain 當(dāng)用戶登錄時(shí)會(huì)進(jìn)入此類的loadUserByUsername方法對(duì)用戶進(jìn)行驗(yàn)證转捕,驗(yàn)證成功后會(huì)被保存在當(dāng)前回話的principal對(duì)象中
* 系統(tǒng)獲取當(dāng)前登錄對(duì)象信息方法 WebUserDetails webUserDetails = (WebUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
*
*/
@Configuration
@EnableWebSecurity //啟動(dòng)web安全性
@EnableGlobalMethodSecurity(prePostEnabled = true) //開(kāi)啟方法級(jí)的權(quán)限注解 性設(shè)置后控制器層的方法前的@PreAuthorize("hasRole('admin')") 注解才能起效
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailService myUserDetailService;
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Value("${jwt.route.authentication.path}")
private String authenticationPath;
// 不需要認(rèn)證的接口
@Value("${com.example.oauth.security.antMatchers}")
private String antMatchers;
/**
* 置user-detail服務(wù)
*
* 方法描述
* accountExpired(boolean) 定義賬號(hào)是否已經(jīng)過(guò)期
* accountLocked(boolean) 定義賬號(hào)是否已經(jīng)鎖定
* and() 用來(lái)連接配置
* authorities(GrantedAuthority...) 授予某個(gè)用戶一項(xiàng)或多項(xiàng)權(quán)限
* authorities(List) 授予某個(gè)用戶一項(xiàng)或多項(xiàng)權(quán)限
* authorities(String...) 授予某個(gè)用戶一項(xiàng)或多項(xiàng)權(quán)限
* disabled(boolean) 定義賬號(hào)是否已被禁用
* withUser(String) 定義用戶的用戶名
* password(String) 定義用戶的密碼
* roles(String...) 授予某個(gè)用戶一項(xiàng)或多項(xiàng)角色
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
// 配置指定用戶權(quán)限信息 通常生產(chǎn)環(huán)境都是從數(shù)據(jù)庫(kù)中讀取用戶權(quán)限信息而不是在這里配置
//auth.inMemoryAuthentication().withUser("username1").password("123456").roles("USER").and().withUser("username2").password("123456").roles("USER","AMDIN");
// **************** 基于數(shù)據(jù)庫(kù)中的用戶權(quán)限信息 進(jìn)行認(rèn)證
//指定密碼加密所使用的加密器為 bCryptPasswordEncoder()
//需要將密碼加密后寫(xiě)入數(shù)據(jù)庫(kù)
// myUserDetailService 類中獲取了用戶的用戶名、密碼以及是否啟用的信息唆垃,查詢用戶所授予的權(quán)限五芝,用來(lái)進(jìn)行鑒權(quán),查詢用戶作為群組成員所授予的權(quán)限
auth.userDetailsService(myUserDetailService).passwordEncoder(bCryptPasswordEncoder());
//不刪除憑據(jù)辕万,以便記住用戶
auth.eraseCredentials(false);
}
/**
* 配置Spring Security的Filter鏈
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//解決靜態(tài)資源被攔截的問(wèn)題
web.ignoring().antMatchers("/favicon.ico");
web.ignoring().antMatchers("/error");
super.configure(web);
}
/**
* 解決 無(wú)法直接注入 AuthenticationManager
* @return
* @throws Exception
*/
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置如何通過(guò)攔截器保護(hù)請(qǐng)求
* 指定哪些請(qǐng)求需要認(rèn)證枢步,哪些請(qǐng)求不需要認(rèn)證,以及所需要的權(quán)限
* 通過(guò)調(diào)用authorizeRequests()和anyRequest().authenticated()就會(huì)要求所有進(jìn)入應(yīng)用的HTTP請(qǐng)求都要進(jìn)行認(rèn)證
*
* 方法描述
* anonymous() 允許匿名用戶訪問(wèn)
* authenticated() 允許經(jīng)過(guò)認(rèn)證的用戶訪問(wèn)
* denyAll() 無(wú)條件拒絕所有訪問(wèn)
* fullyAuthenticated() 如果用戶是完整的話(不是通過(guò)Remember-me功能認(rèn)證的)渐尿,就允許訪問(wèn)
* hasAnyAuthority(String...) 如果用戶具備給定權(quán)限中的某一個(gè)的話醉途,就允許訪問(wèn)
* hasAnyRole(String...) 如果用戶具備給定角色中的某一個(gè)的話,就允許訪問(wèn)
* hasAuthority(String) 如果用戶具備給定權(quán)限的話砖茸,就允許訪問(wèn)
* hasIpAddress(String) 如果請(qǐng)求來(lái)自給定IP地址的話隘擎,就允許訪問(wèn)
* hasRole(String) 如果用戶具備給定角色的話,就允許訪問(wèn)
* not() 對(duì)其他訪問(wèn)方法的結(jié)果求反
* permitAll() 無(wú)條件允許訪問(wèn)
* rememberMe() 如果用戶是通過(guò)Remember-me功能認(rèn)證的凉夯,就允許訪問(wèn)
*
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("不需要認(rèn)證的url:"+antMatchers);
//super.configure(http);
//關(guān)閉csrf驗(yàn)證
http.csrf().disable()
// 基于token货葬,所以不需要session 如果基于session 則表使用這段代碼
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//對(duì)請(qǐng)求進(jìn)行認(rèn)證 url認(rèn)證配置順序?yàn)椋?.先配置放行不需要認(rèn)證的 permitAll() 2.然后配置 需要特定權(quán)限的 hasRole() 3.最后配置 anyRequest().authenticated()
.authorizeRequests()
// 所有 /oauth/v1/api/login/ 請(qǐng)求的都放行 不做認(rèn)證即不需要登錄即可訪問(wèn)
.antMatchers(antMatchers.split(",")).permitAll()
//.antMatchers("/auth/v1/api/login/**","/auth/v1/api/module/tree/**","/auth/v1/api/grid/**").permitAll()
// 對(duì)于獲取token的rest api要允許匿名訪問(wèn)
.antMatchers("oauth/**").permitAll()
// 其他請(qǐng)求都需要進(jìn)行認(rèn)證,認(rèn)證通過(guò)夠才能訪問(wèn) 待考證:如果使用重定向 httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse); 重定向跳轉(zhuǎn)的url不會(huì)被攔截(即在這里配置了重定向的url需要特定權(quán)限認(rèn)證不起效),但是如果在Controller 方法上配置了方法級(jí)的權(quán)限則會(huì)進(jìn)行攔截
.anyRequest().authenticated()
.and().exceptionHandling()
// 認(rèn)證配置當(dāng)用戶請(qǐng)求了一個(gè)受保護(hù)的資源劲够,但是用戶沒(méi)有通過(guò)登錄認(rèn)證震桶,則拋出登錄認(rèn)證異常,MyAuthenticationEntryPointHandler類中commence()就會(huì)調(diào)用
.authenticationEntryPoint(myAuthenticationEntryPoint())
//用戶已經(jīng)通過(guò)了登錄認(rèn)證征绎,在訪問(wèn)一個(gè)受保護(hù)的資源蹲姐,但是權(quán)限不夠,則拋出授權(quán)異常,MyAccessDeniedHandler類中handle()就會(huì)調(diào)用
.accessDeniedHandler(myAccessDeniedHandler())
.and()
//
.formLogin()
// 登錄url
.loginProcessingUrl("/auth/v1/api/login/entry") // 此登錄url 和Controller 無(wú)關(guān)系
// .loginProcessingUrl("/auth/v1/api/login/enter") //使用自己定義的Controller 中的方法 登錄會(huì)進(jìn)入Controller 中的方法
// username參數(shù)名稱 后臺(tái)接收前端的參數(shù)名
.usernameParameter("userAccount")
//登錄密碼參數(shù)名稱 后臺(tái)接收前端的參數(shù)名
.passwordParameter("userPwd")
//登錄成功跳轉(zhuǎn)路徑
.successForwardUrl("/")
//登錄失敗跳轉(zhuǎn)路徑
.failureUrl("/")
//登錄頁(yè)面路徑
.loginPage("/")
.permitAll()
//登錄成功后 MyAuthenticationSuccessHandler類中onAuthenticationSuccess()被調(diào)用
.successHandler(myAuthenticationSuccessHandler())
//登錄失敗后 MyAuthenticationFailureHandler 類中onAuthenticationFailure()被調(diào)用
.failureHandler(myAuthenticationFailureHandler())
.and()
.logout()
//退出系統(tǒng)url
.logoutUrl("/auth/v1/api/login/logout")
//退出系統(tǒng)后的url跳轉(zhuǎn)
.logoutSuccessUrl("/")
//退出系統(tǒng)后的 業(yè)務(wù)處理
.logoutSuccessHandler(myLogoutSuccessHandler())
.permitAll()
.invalidateHttpSession(true)
.and()
//登錄后記住用戶淤堵,下次自動(dòng)登錄,數(shù)據(jù)庫(kù)中必須存在名為persistent_logins的表
// 勾選Remember me登錄會(huì)在PERSISTENT_LOGINS表中寝衫,生成一條記錄
.rememberMe()
//cookie的有效期(秒為單位
.tokenValiditySeconds(3600);
// 加入自定義UsernamePasswordAuthenticationFilter替代原有Filter
http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//在 beforeFilter 之前添加 自定義 filter
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
// 添加JWT filter 驗(yàn)證其他請(qǐng)求的Token是否合法
http.addFilterBefore(authenticationTokenFilterBean(), FilterSecurityInterceptor.class);
// 禁用緩存
http.headers().cacheControl();
}
/**
* 密碼加密方式
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 注冊(cè) 登錄認(rèn)證 bean
* @return
*/
@Bean
public AuthenticationEntryPoint myAuthenticationEntryPoint(){
//return new MyAuthenticationEntryPointHandler();
return new JwtAuthenticationEntryPoint();
}
/**
* 注冊(cè) 認(rèn)證權(quán)限不足處理 bean
* @return
*/
@Bean
public AccessDeniedHandler myAccessDeniedHandler(){
return new MyAccessDeniedHandler();
}
/**
* 注冊(cè) 登錄成功 處理 bean
* @return
*/
@Bean
public AuthenticationSuccessHandler myAuthenticationSuccessHandler(){
return new MyAuthenticationSuccessHandler();
}
/**
* 注冊(cè) 登錄失敗 處理 bean
* @return
*/
@Bean
public AuthenticationFailureHandler myAuthenticationFailureHandler(){
return new MyAuthenticationFailureHandler();
}
/**
* 注冊(cè) 退出系統(tǒng)成功 處理bean
* @return
*/
@Bean
public LogoutSuccessHandler myLogoutSuccessHandler(){
return new MyLogoutSuccessHandler();
}
/**
* 注冊(cè)jwt 認(rèn)證
* @return
* @throws Exception
*/
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
// JwtAuthenticationTokenFilter 過(guò)濾器被配置為跳過(guò)這個(gè)點(diǎn):/auth/v1/api/login/retrieve/pwd 和 /auth/v1/api/login/entry 不進(jìn)行token 驗(yàn)證. 通過(guò) SkipPathRequestMatcher 實(shí)現(xiàn) RequestMatcher 接口來(lái)實(shí)現(xiàn)。
List<String> pathsToSkip = Arrays.asList("/auth/v1/api/login/retrieve/pwd","/auth/v1/api/login/entry","/auth/v1/api/login/enter"); //不需要token 驗(yàn)證的url
String processingPath = "/auth/v1/api/**"; // 需要驗(yàn)證token 的url
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, processingPath);
return new JwtAuthenticationTokenFilter(matcher);
}
/**
* 驗(yàn)證登錄驗(yàn)證碼
* @return
* @throws Exception
*/
@Bean
public UsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() throws Exception {
return new MyUsernamePasswordAuthenticationFilter(authenticationManagerBean(),myAuthenticationSuccessHandler(),myAuthenticationFailureHandler());
}
}
MyAccessDeniedHandler 自定義權(quán)限不足處理類
/***
*
* @FileName: MyAccessDeniedHandler
* @Company:
* @author ljy
* @Date 2018年05月15日
* @version 1.0.0
* @remark: 自定義權(quán)限不足 需要做的業(yè)務(wù)操作
* @explain 當(dāng)用戶登錄系統(tǒng)后訪問(wèn)資源時(shí)因權(quán)限不足則會(huì)進(jìn)入到此類并執(zhí)行相關(guān)業(yè)務(wù)
*
*/
@Slf4j
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
StringBuffer msg = new StringBuffer("請(qǐng)求: ");
msg.append(httpServletRequest.getRequestURI()).append(" 權(quán)限不足拐邪,無(wú)法訪問(wèn)系統(tǒng)資源.");
log.info(msg.toString());
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.AUTHORITY,msg.toString());
/* boolean ajaxRequest = HttpUtils.isAjaxRequest(httpServletRequest);
if (ajaxRequest){
//如果是ajax請(qǐng)求 則返回403錯(cuò)
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.AUTHORITY,msg.toString());
}else {
// 非ajax請(qǐng)求 則跳轉(zhuǎn)到指定的403頁(yè)面
//此處省略...................
}*/
}
}
MyAuthenticationEntryPointHandler 認(rèn)證失敗處理類
/***
*
* @FileName: MyAuthenticationEntryPointHandler
* @Company:
* @author ljy
* @Date 2018年05月15日
* @version 1.0.0
* @remark: 認(rèn)證失敗 需要做的業(yè)務(wù)操作
* @explain 當(dāng)檢測(cè)到用戶訪問(wèn)系統(tǒng)資源認(rèn)證失敗時(shí)則會(huì)進(jìn)入到此類并執(zhí)行相關(guān)業(yè)務(wù)
*
*/
@Slf4j
@Component
public class MyAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
StringBuffer msg = new StringBuffer("請(qǐng)求訪問(wèn): ");
msg.append(httpServletRequest.getRequestURI()).append(" 接口慰毅, 因?yàn)榈卿洺瑫r(shí),無(wú)法訪問(wèn)系統(tǒng)資源.");
log.info(msg.toString());
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.LOGIN_WITHOUT,msg.toString());
/* boolean ajaxRequest = HttpUtils.isAjaxRequest(httpServletRequest);
if (ajaxRequest){
//如果是ajax請(qǐng)求 則返回自定義錯(cuò)誤
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.LOGIN,map);
}else {
// 非ajax請(qǐng)求 則跳轉(zhuǎn)到指定的403頁(yè)面
//此處省略...................
}*/
}
}
MyAuthenticationException 異常
public class MyAuthenticationException extends AuthenticationException {
public MyAuthenticationException(String msg, Throwable t) {
super(msg, t);
}
public MyAuthenticationException(String msg) {
super(msg);
}
/**
* 加入錯(cuò)誤狀態(tài)值
* @param exceptionEnum
*/
public MyAuthenticationException(ErrorCodeEnum exceptionEnum) {
super(exceptionEnum.getMessage());
}
}
MyAuthenticationFailureHandler 登錄失敗處理類
/***
*
* @FileName: MyAuthenticationFailureHandler
* @Company:
* @author ljy
* @Date 2018年05月15日
* @version 1.0.0
* @remark: 用戶登錄系統(tǒng)失敗后 需要做的業(yè)務(wù)操作
* @explain 當(dāng)用戶登錄系統(tǒng)失敗后則會(huì)進(jìn)入到此類并執(zhí)行相關(guān)業(yè)務(wù)
*
*/
@Slf4j
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//用戶登錄時(shí)身份認(rèn)證未通過(guò)
if (e instanceof BadCredentialsException){
log.info("用戶登錄時(shí):用戶名或者密碼錯(cuò)誤.");
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.LOGIN_INCORRECT);
}else{
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.LOGIN_FAIL);
}
}
}
MyAuthenticationSuccessHandler 登錄成功處理類
/***
*
* @FileName: MyAuthenticationSuccessHandler
* @Company:
* @author ljy
* @Date 2018年05月15日
* @version 1.0.0
* @remark: 用戶登錄系統(tǒng)成功后 需要做的業(yè)務(wù)操作
* @explain 當(dāng)用戶登錄系統(tǒng)成功后則會(huì)進(jìn)入到此類并執(zhí)行相關(guān)業(yè)務(wù)
*
*/
@Slf4j
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisUtil redisUtil;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Value("${jwt.expiration}")
private Long expiration;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//獲得授權(quán)后可得到用戶信息(非jwt 方式)
//User userDetails = (User) authentication.getPrincipal();
//獲得授權(quán)后可得到用戶信息(jwt 方式)
JWTUserDetails userDetails = (JWTUserDetails) authentication.getPrincipal();
//將身份 存儲(chǔ)到SecurityContext里
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
httpServletRequest.getSession().setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
StringBuffer msg = new StringBuffer("用戶:");
msg.append(userDetails.getUsername()).append(" 成功登錄系統(tǒng).");
log.info(msg.toString());
//使用jwt生成token 用于權(quán)限效驗(yàn)
String token = jwtTokenUtil.generateAccessToken(userDetails);
UserDetail user = userDetails.getUserInfo();
user.setToken(token);
//將登錄人信息放在redis中
this.saveTokenToRedis(user.getAccountId(),token,JSON.toJSONString(user));
String access_token = tokenHead+token;
String refresh_token = tokenHead+jwtTokenUtil.refreshToken(token);
Map<String,String> map = new HashMap<>();
map.put("access_token", access_token);
map.put("refresh_token", refresh_token);
map.put("userId",user.getAccountId().toString());
map.put("userName",user.getUserName());
map.put("email",user.getUserEmail());
map.put("msage",msg.toString());
RestfulVo restfulVo = ResultUtil.resultInfo(ErrorCodeEnum.SUCCESS,map);
ResultUtil.writeJavaScript(httpServletResponse,restfulVo);
}
/**
* 將用戶token 和用戶信息 保存到redis中
* @param userId 用戶id
* @param token 用戶token
* @param value 用戶信息
*/
private void saveTokenToRedis(Long userId,String token,String value){
String userKey = RedisKeys.USER_KEY;
redisUtil.hset(userKey,token,value,expiration);
}
}
MyLogoutSuccessHandler 退出成功后處理類
/***
*
* @FileName: MyLogoutSuccessHandler
* @Company:
* @author ljy
* @Date 2018年05月15日
* @version 1.0.0
* @remark: 用戶退出系統(tǒng)成功后 需要做的業(yè)務(wù)操作
* @explain 當(dāng)用戶退出系統(tǒng)成功后則會(huì)進(jìn)入到此類并執(zhí)行相關(guān)業(yè)務(wù)
*
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private RedisUtil redisUtil;
@Autowired
private UserUtils userUtils;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//根據(jù)token清空redis
String userKey = RedisKeys.USER_KEY;
String token = userUtils.getUserToken(httpServletRequest);
redisUtil.hdel(userKey,token);
SecurityContextHolder.clearContext(); //清空上下文
httpServletRequest.getSession().removeAttribute("SPRING_SECURITY_CONTEXT"); // 從session中移除
//退出信息插入日志記錄表中
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.SUCCESS,"退出系統(tǒng)成功.");
}
}
MyUsernamePasswordAuthenticationFilter 校驗(yàn)驗(yàn)證碼
/***
*
* @FileName: MyUsernamePasswordAuthenticationFilter
* @Company:
* @author ljy
* @Date 2018年07月15日
* @version 1.0.0
* @remark: 自定義 登錄校驗(yàn)
* @explain 調(diào)用登錄接口時(shí)會(huì)進(jìn)入到此類的attemptAuthentication方法 進(jìn)行相關(guān)校驗(yàn)操作
*
*/
@Slf4j
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public MyUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager,AuthenticationSuccessHandler successHandler,AuthenticationFailureHandler failureHandler){
this.setFilterProcessesUrl("/auth/v1/api/login/entry"); //這句代碼很重要扎阶,設(shè)置登陸的url 要和 WebSecurityConfig 配置類中的.loginProcessingUrl("/auth/v1/api/login/entry") 一致汹胃,如果不配置則無(wú)法執(zhí)行 重寫(xiě)的attemptAuthentication 方法里面而是執(zhí)行了父類UsernamePasswordAuthenticationFilter的attemptAuthentication()
this.setAuthenticationManager(authenticationManager); // AuthenticationManager 是必須的
this.setAuthenticationSuccessHandler(successHandler); //設(shè)置自定義登陸成功后的業(yè)務(wù)處理
this.setAuthenticationFailureHandler(failureHandler); //設(shè)置自定義登陸失敗后的業(yè)務(wù)處理
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//校驗(yàn)驗(yàn)證碼
String verifyCode = request.getParameter("verifyCode");
if(!checkValidateCode(verifyCode)){
ResultUtil.writeJavaScript(response,ErrorCodeEnum.FAIL,"驗(yàn)證碼錯(cuò)誤.");
return null;
}
//設(shè)置獲取 username 的屬性字段 js傳到后臺(tái)接收數(shù)據(jù)的參數(shù)名
this.setUsernameParameter("userAccount");
//設(shè)置獲取password 的屬性字段 js傳到后臺(tái)接收數(shù)據(jù)的參數(shù)名
this.setPasswordParameter("userPwd");
return super.attemptAuthentication(request, response);
}
/**
* 驗(yàn)證 驗(yàn)證碼是否正確
* @param verifyCode
* @return
*/
private boolean checkValidateCode(String verifyCode){
if(StringUtils.isBlank(verifyCode) || !verifyCode.trim().equals("1234")){
return false;
}
return true;
}
}
整合JWT
JwtTokenUtil jwt 工具類
/***
*
* @FileName: JwtTokenUtil
* @Company:
* @author ljy
* @Date 2018年05月12日
* @version 1.0.0
* @remark: jwt工具類 提供校驗(yàn)toeken 、生成token东臀、根據(jù)token獲取用戶等方法
*
*/
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -5883980282405596071L;
//
public static final String ROLE_REFRESH_TOKEN = "ROLE_REFRESH_TOKEN";
private static final String CLAIM_KEY_USER_ID = "user_id";
private static final String CLAIM_KEY_AUTHORITIES = "scope";
private static final String CLAIM_KEY_ACCOUNT_ENABLED = "enabled";
private static final String CLAIM_KEY_ACCOUNT_NON_LOCKED = "non_locked";
private static final String CLAIM_KEY_ACCOUNT_NON_EXPIRED = "non_expired";
private static final String CLAIM_KEY_USER_ACCOUNT = "sub";
private static final String CLAIM_KEY_CREATED = "created";
//簽名方式
private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
//密匙
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access_token}")
private Long access_token_expiration;
@Value("${jwt.refresh_token}")
private Long refresh_token_expiration;
//過(guò)期時(shí)間
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根據(jù)token 獲取用戶信息
* @param token
* @return
*/
public JWTUserDetails getUserFromToken(String token) {
JWTUserDetails jwtUserDetails;
try {
final Claims claims = getClaimsFromToken(token);
long userId = getUserIdFromToken(token);
String username = claims.getSubject();
List<?> roles = (List<?>) claims.get(CLAIM_KEY_AUTHORITIES);
Collection<? extends GrantedAuthority> authorities = parseArrayToAuthorities(roles);
boolean account_enabled = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_ENABLED);
boolean account_non_locked = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_NON_LOCKED);
boolean account_non_expired = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_NON_EXPIRED);
User user = new User(username, "", account_enabled, account_non_expired, account_non_expired, account_non_locked, authorities);
jwtUserDetails = JWTUserDetailsFactory.create(user, userId,Instant.now());
} catch (Exception e) {
jwtUserDetails = null;
}
return jwtUserDetails;
}
/**
* 根據(jù)token 獲取用戶ID
* @param token
* @return
*/
public long getUserIdFromToken(String token) {
long userId;
try {
final Claims claims = getClaimsFromToken(token);
userId = Long.valueOf(claims != null ? claims.get(CLAIM_KEY_USER_ID).toString() :"0");
} catch (Exception e) {
e.printStackTrace();
userId = 0;
}
return userId;
}
/**
* 根據(jù)token 獲取用戶名
* @param token
* @return
*/
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 根據(jù)token 獲取生成時(shí)間
* @param token
* @return
*/
public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = claims.getIssuedAt();
} catch (Exception e) {
created = null;
}
return created;
}
/**
* 根據(jù)token 獲取過(guò)期時(shí)間
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
/***
* 解析token 信息
* @param token
* @return
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret) //簽名的key
.parseClaimsJws(token) // 簽名token
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成失效時(shí)間
* @param expiration
* @return
*/
private Date generateExpirationDate(long expiration) {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* token 是否過(guò)期
* @param token
* @return
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 生成時(shí)間是否在最后修改時(shí)間之前
* @param created 生成時(shí)間
* @param lastPasswordReset 最后修改密碼時(shí)間
* @return
*/
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
/**
* 根據(jù)用戶信息 生成token
* @param userDetails
* @return
*/
public String generateAccessToken(UserDetails userDetails) {
JWTUserDetails user = (JWTUserDetails) userDetails;
Map<String, Object> claims = generateClaims(user);
claims.put(CLAIM_KEY_AUTHORITIES, JSON.toJSON(authoritiesToArray(user.getAuthorities())));
return generateAccessToken(user.getUsername(), claims);
}
/**
* 重置(更新)token 過(guò)期時(shí)間
* @param token
* @param expiration
*/
public String restTokenExpired(String token,long expiration){
final Claims claims = getClaimsFromToken(token);
Jwts.builder()
.setClaims(claims) //一個(gè)map 可以資源存放東西進(jìn)去
.setSubject(claims.getSubject()) // 用戶名寫(xiě)入標(biāo)題
.setExpiration(new Date(expiration));
//claims.setExpiration(new Date(expiration));
// String refreshedToken = generateAccessToken(claims.getSubject(), claims,expiration);
return "";
}
private Map<String, Object> generateClaims(JWTUserDetails user) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USER_ID, user.getUserId());
claims.put(CLAIM_KEY_ACCOUNT_ENABLED, user.isEnabled());
claims.put(CLAIM_KEY_ACCOUNT_NON_LOCKED, user.isAccountNonLocked());
claims.put(CLAIM_KEY_ACCOUNT_NON_EXPIRED, user.isAccountNonExpired());
return claims;
}
/**
* 生成token
* @param subject 用戶名
* @param claims
* @return
*/
private String generateAccessToken(String subject, Map<String, Object> claims) {
return generateToken(subject, claims, access_token_expiration);
}
/**
* 生成token
* @param subject 用戶名
* @param claims
* @return
*/
private String generateAccessToken(String subject, Map<String, Object> claims,long expiration) {
return generateToken(subject, claims, expiration);
}
/**
* 用戶所擁有的資源權(quán)限
* @param authorities
* @return
*/
private List<?> authoritiesToArray(Collection<? extends GrantedAuthority> authorities) {
List<String> list = new ArrayList<>();
for (GrantedAuthority ga : authorities) {
list.add(ga.getAuthority());
}
return list;
}
private Collection<? extends GrantedAuthority> parseArrayToAuthorities(List<?> roles) {
Collection<GrantedAuthority> authorities = new ArrayList<>();
SimpleGrantedAuthority authority;
for (Object role : roles) {
authority = new SimpleGrantedAuthority(role.toString());
authorities.add(authority);
}
return authorities;
}
/**
* 根據(jù)用戶信息 重新獲取token
* @param userDetails
* @return
*/
public String generateRefreshToken(UserDetails userDetails) {
JWTUserDetails user = (JWTUserDetails) userDetails;
Map<String, Object> claims = generateClaims(user);
// 只授于更新 token 的權(quán)限
String roles[] = new String[]{ROLE_REFRESH_TOKEN};
claims.put(CLAIM_KEY_AUTHORITIES, JSON.toJSON(roles));
return generateRefreshToken(user.getUsername(), claims);
}
/**
* 重新獲取token
* @param subject 用戶名
* @param claims
* @return
*/
private String generateRefreshToken(String subject, Map<String, Object> claims) {
return generateToken(subject, claims, refresh_token_expiration);
}
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getCreatedDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& (!isTokenExpired(token));
}
/**
* 刷新重新獲取token
* @param token 源token
* @return
*/
public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
refreshedToken = generateAccessToken(claims.getSubject(), claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
private String generateToken(String subject, Map<String, Object> claims, long expiration) {
return Jwts.builder()
.setClaims(claims) //一個(gè)map 可以資源存放東西進(jìn)去
.setSubject(subject) // 用戶名寫(xiě)入標(biāo)題
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date())
.setExpiration(generateExpirationDate(expiration)) //過(guò)期時(shí)間
//.setNotBefore(now) //系統(tǒng)時(shí)間之前的token都是不可以被承認(rèn)的
.signWith(SIGNATURE_ALGORITHM, secret) //數(shù)字簽名
.compact();
}
/**
* 驗(yàn)證token 是否合法
* @param token token
* @param userDetails 用戶信息
* @return
*/
public Boolean validateToken(String token, UserDetails userDetails) {
JWTUserDetails user = (JWTUserDetails) userDetails;
final long userId = getUserIdFromToken(token);
final String username = getUsernameFromToken(token);
// final Date created = getCreatedDateFromToken(token);
// final Date expiration = getExpirationDateFromToken(token);
return (userId == user.getUserId()
&& username.equals(user.getUsername())
&& !isTokenExpired(token)
/* && !isCreatedBeforeLastPasswordReset(created, userDetails.getLastPasswordResetDate()) */
);
}
}
JWTUserDetails 用戶信息
/***
*
* @FileName: JWTUserDetails
* @Company:
* @author ljy
* @Date 2018年05月120日
* @version 1.0.0
* @remark: jwt用戶信息
* @explain Spring Security需要我們實(shí)現(xiàn)幾個(gè)東西着饥,第一個(gè)是UserDetails:這個(gè)接口中規(guī)定了用戶的幾個(gè)必須要有的方法,所以我們創(chuàng)建一個(gè)JwtUser類來(lái)實(shí)現(xiàn)這個(gè)接口惰赋。為什么不直接使用User類宰掉?因?yàn)檫@個(gè)UserDetails完全是為了安全服務(wù)的,它和我們的領(lǐng)域類可能有部分屬性重疊赁濒,但很多的接口其實(shí)是安全定制的轨奄,所以最好新建一個(gè)類:
*
*/
public class JWTUserDetails implements UserDetails {
private Long userId; //用戶ID
private String password; //用戶密碼
private final String username; //用戶名
private final Collection<? extends GrantedAuthority> authorities; //用戶角色權(quán)限
private final Boolean isAccountNonExpired; //賬號(hào)是否過(guò)期
private final Boolean isAccountNonLocked; //賬戶是否鎖定
private final Boolean isCredentialsNonExpired; //密碼是否過(guò)期
private Boolean enabled; //是否激活
private final Instant lastPasswordResetDate; //上次密碼重置時(shí)間
private UserDetail userInfo;
public JWTUserDetails(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities,Instant lastPasswordResetDate) {
this(userId, username, password, true, true, true, true, authorities,lastPasswordResetDate);
}
public JWTUserDetails(UserDetail userInfo, Collection<? extends GrantedAuthority> authorities) {
this.userInfo = userInfo;
if (userInfo != null && StringUtils.isNotBlank(userInfo.getUserAccount())) {
this.userId = userInfo.getAccountId();
this.username = userInfo.getUserAccount();
this.password = userInfo.getUserPwd();
this.enabled = userInfo.getStatus() == 0 ? false : true;
this.isAccountNonExpired = true;
this.isAccountNonLocked = true;
this.isCredentialsNonExpired = true;
this.authorities = authorities;
this.lastPasswordResetDate = userInfo.getLastPasswordResetDate();
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
public JWTUserDetails(Long userId, String username, String password, boolean enabled, boolean isAccountNonExpired, boolean isCredentialsNonExpired, boolean isAccountNonLocked, Collection<? extends GrantedAuthority> authorities,Instant lastPasswordResetDate) {
if (username != null && !"".equals(username) && password != null) {
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.isAccountNonExpired = isAccountNonExpired;
this.isAccountNonLocked = isAccountNonLocked;
this.isCredentialsNonExpired = isCredentialsNonExpired;
this.authorities = authorities;
this.lastPasswordResetDate = lastPasswordResetDate;
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return isCredentialsNonExpired;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return enabled;
}
@JsonIgnore
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
@JsonIgnore
public Instant getLastPasswordResetDate() {
return lastPasswordResetDate;
}
public UserDetail getUserInfo() {
return userInfo;
}
public void setUserInfo(UserDetail userInfo) {
this.userInfo = userInfo;
}
}
JWTUserDetailsFactory
/***
*
* @FileName: JWTUserDetailsFactory
* @Company:
* @author ljy
* @Date 2018年05月120日
* @version 1.0.0
* @remark: 負(fù)責(zé)創(chuàng)建JWTUserDetails 對(duì)象
*
*/
public final class JWTUserDetailsFactory {
private JWTUserDetailsFactory(){
}
public static JWTUserDetails create(User user, Long userId, Instant date){
return new JWTUserDetails(userId, user.getUsername(), user.getPassword(),user.getAuthorities(), date);
}
public static JWTUserDetails create(User user, UserDetail userDetail){
return new JWTUserDetails(userDetail,user.getAuthorities());
}
private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
JwtAuthenticationTokenFilter 請(qǐng)求接口時(shí)校驗(yàn)token信息
/***
*
* @FileName: JwtAuthenticationTokenFilter
* @Company:
* @author ljy
* @Date 2018年05月120日
* @version 1.0.0
* @remark: jwt認(rèn)證token
* @explain 每次請(qǐng)求接口時(shí) 就會(huì)進(jìn)入這里驗(yàn)證token 是否合法
* token 如果用戶一直在操作,則token 過(guò)期時(shí)間會(huì)疊加 如果超過(guò)設(shè)置的過(guò)期時(shí)間未操作 則token 失效 需要重新登錄
*
*/
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource(name = "myUserDetailService")
private UserDetailsService userDetailsService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Value("${jwt.expiration}")
private Long expiration;
private RequestMatcher authenticationRequestMatcher;
public JwtAuthenticationTokenFilter() {
}
public JwtAuthenticationTokenFilter(RequestMatcher authenticationRequestMatcher) {
this.authenticationRequestMatcher = authenticationRequestMatcher;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//過(guò)濾掉不需要token驗(yàn)證的url
if(authenticationRequestMatcher != null && !authenticationRequestMatcher.matches(httpServletRequest)){
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
String authHeader = httpServletRequest.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenHead)) {
final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
log.info("請(qǐng)求"+httpServletRequest.getRequestURI()+"攜帶的token值:" + authToken);
// 查看redis中的token信息是否過(guò)期
boolean isExists = redisUtil.hexists(RedisKeys.USER_KEY,authToken);
if (!isExists){
// token 過(guò)期 提示用戶登錄超時(shí) 重新登錄系統(tǒng)
//throw new MyAuthenticationException(ErrorCodeEnum.LOGIN_WITHOUT);
}
//如果在token過(guò)期之前觸發(fā)接口,我們更新token過(guò)期時(shí)間拒炎,token值不變只更新過(guò)期時(shí)間
Date createTokenDate = jwtTokenUtil.getCreatedDateFromToken(authToken); //獲取token生成時(shí)間
log.info("createTokenDate: " + createTokenDate);
if(createTokenDate != null){
Duration between = Duration.between(Instant.now(), Instant.ofEpochMilli(createTokenDate.getTime()));
Long differSeconds = between.toMillis();
boolean isExpire = expiration > differSeconds;
if (isExpire) { //如果 請(qǐng)求接口時(shí)間在token 過(guò)期之前 則更新token過(guò)期時(shí)間 我們可以將用戶的token 存放到redis 中,更新redis 的過(guò)期時(shí)間
//更新 延長(zhǎng) redis 中的過(guò)期時(shí)間
}
}
String useraccount = jwtTokenUtil.getUsernameFromToken(authToken);
log.info("JwtAuthenticationTokenFilter[doFilterInternal] checking authentication " + useraccount);
//token校驗(yàn)通過(guò)
if(useraccount != null){
TokenUtils.setToken(authToken);//設(shè)置token
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication == null || authentication.getPrincipal().equals("anonymousUser")){
//根據(jù)account去數(shù)據(jù)庫(kù)中查詢user數(shù)據(jù)挪拟,足夠信任token的情況下,可以省略這一步
UserDetails userDetails = this.userDetailsService.loadUserByUsername(useraccount);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
httpServletRequest));
log.info("JwtAuthenticationTokenFilter[doFilterInternal] authenticated user " + useraccount + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthentication);
}
}else {
log.info("當(dāng)前請(qǐng)求用戶信息:"+ JSON.toJSONString(authentication.getPrincipal()));
}
}else {
log.info("token 無(wú)效.");
throw new MyAuthenticationException(ErrorCodeEnum.TOKEN_INVALID);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
JwtAuthenticationEntryPoint jwt認(rèn)證失敗處理
/***
*
* @FileName: JwtAuthenticationEntryPoint
* @Company:
* @author ljy
* @Date 2018年05月120日
* @version 1.0.0
* @remark: jwt 認(rèn)證處理類
*
*/
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
StringBuffer msg = new StringBuffer("請(qǐng)求訪問(wèn): ");
msg.append(httpServletRequest.getRequestURI()).append(" 接口击你, 經(jīng)jwt 認(rèn)證失敗玉组,無(wú)法訪問(wèn)系統(tǒng)資源.");
log.info(msg.toString());
log.info(e.toString());
// 用戶登錄時(shí)身份認(rèn)證未通過(guò)
if(e instanceof BadCredentialsException) {
log.info("用戶登錄時(shí)身份認(rèn)證失敗.");
ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.LOGIN_INCORRECT, msg.toString());
}else if(e instanceof InsufficientAuthenticationException){
log.info("缺少請(qǐng)求頭參數(shù),Authorization傳遞是token值所以參數(shù)是必須的.");
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.NO_TOKEN,msg.toString());
}else {
log.info("用戶token無(wú)效.");
ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.TOKEN_INVALID,msg.toString());
}
}
}
工具類
SkipPathRequestMatcher 忽略不進(jìn)行token校驗(yàn)的url
public class SkipPathRequestMatcher implements RequestMatcher {
private OrRequestMatcher matchers;
private RequestMatcher processingMatcher;
public SkipPathRequestMatcher(List<String> pathsToSkip, String processingPath) {
List<RequestMatcher> m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList());
matchers = new OrRequestMatcher(m);
processingMatcher = new AntPathRequestMatcher(processingPath);
}
@Override
public boolean matches(HttpServletRequest request) {
if (matchers.matches(request)) {
return false;
}
return processingMatcher.matches(request) ? true : false;
}
}
UrlMatcher url匹配接口
public interface UrlMatcher {
Object compile(String paramString);
boolean pathMatchesUrl(Object paramObject, String paramString);
String getUniversalMatchPattern();
boolean requiresLowerCaseUrl();
}
AntUrlPathMatcher url 匹配 在 MyInvocationSecurityMetadataSourceService 類中會(huì)用到
@Component
public class AntUrlPathMatcher implements UrlMatcher {
private boolean requiresLowerCaseUrl;
private PathMatcher pathMatcher;
public AntUrlPathMatcher() {
this(true);
}
public AntUrlPathMatcher(boolean requiresLowerCaseUrl)
{
this.requiresLowerCaseUrl = true;
this.pathMatcher = new AntPathMatcher();
this.requiresLowerCaseUrl = requiresLowerCaseUrl;
}
public Object compile(String path) {
if (this.requiresLowerCaseUrl) {
return path.toLowerCase();
}
return path;
}
public void setRequiresLowerCaseUrl(boolean requiresLowerCaseUrl){
this.requiresLowerCaseUrl = requiresLowerCaseUrl;
}
public boolean pathMatchesUrl(Object path, String url) {
if (("/**".equals(path)) || ("**".equals(path))) {
return true;
}
return this.pathMatcher.match((String)path, url);
}
public String getUniversalMatchPattern() {
return"/**";
}
public boolean requiresLowerCaseUrl() {
return this.requiresLowerCaseUrl;
}
public String toString() {
return super.getClass().getName() + "[requiresLowerCase='"
+ this.requiresLowerCaseUrl + "']";
}
前端使用ajax方式登錄
//登錄頁(yè)面登錄按鈕事件
var handleSignInFormSubmit = function() {
$('#m_login_signin_submit').click(function(e) {
e.preventDefault();
var btn = $(this);
var form = $(this).closest('form');
form.validate({
rules: {
userAccount: {
required: true
},
userPwd: {
required: true
}
},
messages: {
userAccount: {
required: "請(qǐng)輸入登錄用戶."
},
userPwd: {
required: "請(qǐng)輸入登錄密碼."
}
}
});
if (!form.valid()) {
return;
}
btn.addClass('m-loader m-loader--right m-loader--light').attr('disabled', true);
form.ajaxSubmit({
type: 'post',
url: ajaxUrl+'login/entry',
success: function(response, status, xhr, $form) {
console.log(response);
if(response.status != 0){
btn.removeClass('m-loader m-loader--right m-loader--light').attr('disabled', false);
showErrorMsg(form, 'danger', '錯(cuò)誤的用戶名或密碼.');
}else {
//得到登錄后的token
var access_token = response.data.access_token;
localStorage.setItem('user_token', JSON.stringify(access_token));
localStorage.setItem('user', JSON.stringify(response.data));
window.location.href="assets/snippets/pages/home/index.html";
}
},
error:function (response, status, xhr) {
btn.removeClass('m-loader m-loader--right m-loader--light').attr('disabled', false);
showErrorMsg(form, 'danger', '網(wǎng)絡(luò)出現(xiàn)錯(cuò)誤.');
}
});
return false; // 阻止表單自動(dòng)提交事件,必須返回false丁侄,否則表單會(huì)自己再做一次提交操作惯雳,并且頁(yè)面跳轉(zhuǎn)
});
}
攜帶token信息請(qǐng)求其它接口獲取數(shù)據(jù)(以刪除為例)
var userToken = JSON.parse(localStorage.getItem('user_token'));
$.ajax({
url: ajaxUrl+'userRole/del',
data : {
roleId:checkboxRoleId,
userIds:selectDeleteUserIds.toString(),
_method: 'DELETE'
},
type:"POST",
dataType:"json",
headers: {'Authorization': userToken}, //攜帶token到后臺(tái)的方式
success: function(response, status, xhr) {
var serverStatus = response.status;
toastr.clear();
//這里根據(jù)后臺(tái)返回的狀態(tài)做相應(yīng)的處理,后臺(tái)會(huì)根據(jù)token去校驗(yàn)權(quán)限鸿摇,并返回相應(yīng)的響應(yīng)
if(serverStatus == -1){
//登陸超時(shí)吨凑,需要重新登陸系統(tǒng)
toastr.error(response.message);
toastr.info("即將跳轉(zhuǎn)到登陸頁(yè)面.");
window.location.href="../../../../login.html";
}else if (response.status != 0){
// 我設(shè)置的 response.status == 0 表示成功
// response.status != 0 有可能是 無(wú)權(quán)限訪問(wèn)、token錯(cuò)誤户辱、登錄失效等 根據(jù)后臺(tái)認(rèn)證返回
toastr.error(response.message);
}else {
reloadUserGrid();
}
//移除遮罩層
mApp.unblock('#be-authorized-userGrid');
},
error:function (response, status, xhr) {
toastr.error("網(wǎng)絡(luò)出現(xiàn)錯(cuò)誤.");
}
});