權(quán)限控制采用 RBAC
思想懂扼。簡(jiǎn)單地說,一個(gè)用戶擁有若干角色嘹锁,每個(gè)角色擁有一個(gè)默認(rèn)的權(quán)限葫录,每一個(gè)角色擁有若干個(gè)菜單,菜單中存在按鈕權(quán)限领猾,這樣米同,就構(gòu)造成“用戶-角色-菜單” 的授權(quán)模型。在這種模型中摔竿,用戶與角色窍霞、角色與菜單之間構(gòu)成了多對(duì)多的關(guān)系,如下圖
1.簡(jiǎn)介
對(duì)訪問權(quán)限進(jìn)行控制拯坟,應(yīng)用的安全性包括用戶認(rèn)證(Authentication)和用戶授權(quán)(Authorization)兩個(gè)部分但金。用戶認(rèn)證指的是驗(yàn)證某個(gè)用戶是否為系統(tǒng)中的合法主體,也就是說用戶能否訪問該系統(tǒng)郁季。用戶授權(quán)指的是驗(yàn)證某個(gè)用戶是否有權(quán)限執(zhí)行某個(gè)操作冷溃。spring-security的主要核心功能為 認(rèn)證和授權(quán),所有的架構(gòu)也是基于這兩個(gè)核心功能去實(shí)現(xiàn)的梦裂。
2.原理
對(duì)Web資源進(jìn)行保護(hù)似枕,最好的辦法莫過于Filter,要想對(duì)方法調(diào)用進(jìn)行保護(hù)年柠,最好的辦法莫過于AOP凿歼。spring-security在我們進(jìn)行用戶認(rèn)證以及授予權(quán)限的時(shí)候,通過各種各樣的攔截器來控制權(quán)限的訪問冗恨,從而實(shí)現(xiàn)安全答憔。
spring-security的filter如何啟用的,參考Spring Security實(shí)現(xiàn)原理剖析(一):filter的構(gòu)造和初始化
3.核心組件
組件 | 說明 |
---|---|
SecurityContextHolder | 提供對(duì)SecurityContext的訪問 |
SecurityContext | 持有Authentication對(duì)象和其他可能需要的信息 |
AuthenticationManager | 可以包含多個(gè)AuthenticationProvider |
ProviderManager | AuthenticationManager接口的實(shí)現(xiàn)類 |
AuthenticationProvider | 進(jìn)行認(rèn)證操作的類 調(diào)用其中的authenticate()方法去進(jìn)行認(rèn)證操作 |
Authentication | Spring Security方式的認(rèn)證主體 |
GrantedAuthority | 對(duì)認(rèn)證主題的應(yīng)用層面的授權(quán)掀抹,含當(dāng)前用戶的權(quán)限信息虐拓,通常使用角色表示 |
UserDetails | 構(gòu)建Authentication對(duì)象必須的信息,可以自定義傲武,可能需要訪問DB得到 |
UserDetailsService | 通過username構(gòu)建UserDetails對(duì)象蓉驹,通過loadUserByUsername根據(jù)userName獲取UserDetail對(duì)象 (可以在這里基于自身業(yè)務(wù)進(jìn)行自定義的實(shí)現(xiàn) 如通過數(shù)據(jù)庫(kù)城榛,xml,緩存獲取等) |
4.加載機(jī)制
4.1 自定義配置類,繼承WebSecurityConfigurerAdapter态兴,重寫configure方法
package com.zhouy.modules.security.config;
import com.zhouy.annotation.AnonymousAccess;
import com.zhouy.modules.security.security.JwtAuthenticationEntryPoint;
import com.zhouy.modules.security.security.JwtAuthorizationTokenFilter;
import com.zhouy.modules.security.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${jwt.header}")
private String tokenHeader;
// 登錄驗(yàn)證類
private final UserDetailsService jwtUserDetailsService;
// token過濾器來驗(yàn)證token有效性類
private final JwtAuthorizationTokenFilter authorizationTokenFilter;
// 認(rèn)證失敗處理類
private final JwtAuthenticationEntryPoint unauthorizedHandler;
// spring上下文
private final ApplicationContext applicationContext;
public SecurityConfig(@Qualifier("jwtUserDetailsService") UserDetailsService jwtUserDetailsService,
JwtAuthorizationTokenFilter authorizationTokenFilter,
JwtAuthenticationEntryPoint unauthorizedHandler,
ApplicationContext applicationContext){
this.jwtUserDetailsService = jwtUserDetailsService;
this.authorizationTokenFilter = authorizationTokenFilter;
this.unauthorizedHandler = unauthorizedHandler;
this.applicationContext = applicationContext;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoderBean());
}
// @Bean
// GrantedAuthorityDefaults grantedAuthorityDefaults() {
// // Remove the ROLE_ prefix
// return new GrantedAuthorityDefaults("");
// }
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 搜尋 匿名標(biāo)記 url: PreAuthorize("hasAnyRole('anonymous')") 和 PreAuthorize("@el.check('anonymous')") 和 AnonymousAccess
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
Set<String> anonymousUrls = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
PreAuthorize preAuthorize = handlerMethod.getMethodAnnotation(PreAuthorize.class);
if (null != preAuthorize && preAuthorize.value().toLowerCase().contains("anonymous")) {
anonymousUrls.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
} else if (null != anonymousAccess && null == preAuthorize) {
anonymousUrls.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
}
}
httpSecurity
// 禁用 CSRF
.csrf().disable()
// 授權(quán)異常
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 不創(chuàng)建會(huì)話
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 過濾請(qǐng)求
.authorizeRequests()
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).anonymous()
// 放行OPTIONS請(qǐng)求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 自定義匿名訪問所有url放行 : 允許 匿名和帶權(quán)限以及登錄用戶訪問
.antMatchers(anonymousUrls.toArray(new String[0])).permitAll()
// 除了上面外其他所有請(qǐng)求都需要認(rèn)證
.anyRequest().authenticated()
// 防止iframe 造成跨域
.and().headers().frameOptions().disable();
httpSecurity
.addFilterBefore(authorizationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
4.2 spring-security認(rèn)證過程
重寫WebSecurityConfigurerAdapter的configureGlobal(AuthenticationManagerBuilder auth)方法狠持。
UserDetailsService實(shí)現(xiàn)
package com.zhouy.modules.security.service;
import com.zhouy.exception.BadRequestException;
import com.zhouy.modules.security.security.JwtUser;
import com.zhouy.modules.system.service.UserService;
import com.zhouy.modules.system.service.dto.DeptSmallDTO;
import com.zhouy.modules.system.service.dto.JobSmallDTO;
import com.zhouy.modules.system.service.dto.UserDTO;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class JwtUserDetailsService implements UserDetailsService{
private final UserService userService;
private final JwtPermissionService jwtPermissionService;
public JwtUserDetailsService (UserService userService,JwtPermissionService jwtPermissionService){
this.userService = userService;
this.jwtPermissionService = jwtPermissionService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDTO user = userService.findByName(username);
if (user == null){
throw new BadRequestException("賬號(hào)不存在");
}else{
return createJwtUser(user);
}
}
public UserDetails createJwtUser(UserDTO user) {
return new JwtUser(user.getId(),
user.getUsername(),
user.getPassword(),
user.getAvatar(),
user.getEmail(),
user.getPhone(),
Optional.ofNullable(user.getDept()).map(DeptSmallDTO::getName).orElse(null),
Optional.ofNullable(user.getJob()).map(JobSmallDTO::getName).orElse(null),
jwtPermissionService.mapToGrantedAuthorities(user),
user.getEnabled(),
user.getCreateTime(),
user.getLastPasswordResetTime());
}
}
UserDetails
package com.zhouy.modules.security.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;
/**
* UserDetails
*/
@Getter
@AllArgsConstructor
public class JwtUser implements UserDetails {
@JsonIgnore
private final Long id;
private final String username;
@JsonIgnore
private final String password;
private final String avatar;
private final String email;
private final String phone;
private final String dept;
private final String job;
//所擁有的權(quán)限
@JsonIgnore
private final Collection<GrantedAuthority> authorities;
private final boolean enabled;
private Timestamp createTime;
@JsonIgnore
private final Date lastPasswordResetDate;
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public boolean isEnabled() {
return enabled;
}
public Collection getRoles() {
return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
}
}
UserDetails的權(quán)限信息
package com.zhouy.modules.security.service;
import com.zhouy.modules.system.domain.Menu;
import com.zhouy.modules.system.domain.Role;
import com.zhouy.modules.system.mapper.MenuMapper;
import com.zhouy.modules.system.mapper.RoleMapper;
import com.zhouy.modules.system.service.MenuService;
import com.zhouy.modules.system.service.dto.UserDTO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@CacheConfig(cacheNames = "role")
public class JwtPermissionService {
private final RoleMapper roleMapper;
private final MenuService menuService;
public JwtPermissionService(RoleMapper roleMapper,MenuService menuService){
this.roleMapper = roleMapper;
this.menuService = menuService;
}
/**
* #p0.username:入?yún)⒌牡谝粋€(gè)參數(shù)的username屬性值
* @param userDTO
* @return
*/
@Cacheable(key = "'loadPermissionByUser:'+ #p0.username")
public Collection<GrantedAuthority> mapToGrantedAuthorities(UserDTO userDTO){
Set<Role> roles = roleMapper.findByUsers_Id(userDTO.getId());
Set<String> permissions = roles.stream()
.filter(role -> StringUtils.isNoneBlank(role.getPermission()))
.map(Role::getPermission)
.collect(Collectors.toSet());
Set<String> menu_permissions = roles.stream()
.flatMap(role -> menuService.findByRoleId(role.getId()).stream())
.filter(menu -> StringUtils.isNoneBlank(menu.getPermission()))
.map(Menu::getPermission)
.collect(Collectors.toSet());
// Set<String> menu_permissions = new HashSet<>();
// for (Role role:roles) {
// List<Menu> menus = menuService.findByRoleId(role.getId());
// for (Menu menu:menus) {
// if (StringUtils.isNoneBlank(menu.getPermission())){
// menu_permissions.add(menu.getPermission());
// }
// }
// }
permissions.addAll(menu_permissions);
return permissions.stream().map(permission -> new SimpleGrantedAuthority(permission))
.collect(Collectors.toList());
}
}
4.3 “記住我”
用戶可以使用賬號(hào)和密碼進(jìn)行認(rèn)證,但是如果用戶使用賬號(hào)和密碼進(jìn)行認(rèn)證時(shí)選擇了“記住我”功能瞻润,則在有效期內(nèi)工坊,當(dāng)用戶關(guān)閉瀏覽器后再重新訪問服務(wù)時(shí),不需要用戶再次輸入賬號(hào)和密碼重新進(jìn)行認(rèn)證敢订,而是通過“記住我”功能自動(dòng)認(rèn)證。
上述的用戶認(rèn)證處理邏輯都是基于Spring Security提供的默認(rèn)實(shí)現(xiàn)罢吃,我們只需要自己實(shí)現(xiàn)一個(gè)UserDetailsService接口用于獲取用戶認(rèn)證信息即可楚午,十分簡(jiǎn)便。
4.4 spring-security權(quán)限控制過程
安全配置類上添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解尿招,開啟方法級(jí)別的權(quán)限控制矾柜,在方法上添加@PreAuthorize("@el.check('job:list')")類似注解,實(shí)現(xiàn)在方法之前先做權(quán)限驗(yàn)證就谜。
4.4.1 接口權(quán)限
Spring Security
提供了Spring EL
表達(dá)式吱七,允許我們?cè)诙x接口訪問的方法上面添加注解苞轿,來控制訪問權(quán)限,常用的 EL
如下
表達(dá)式 | 說明 |
---|---|
hasRole([role]) | 當(dāng)前用戶是否擁有指定角色 |
hasAnyRole([role1,role2]) | 多個(gè)角色是一個(gè)以逗號(hào)進(jìn)行分隔的字符串。如果當(dāng)前用戶擁有指定角色中的任意一個(gè)則返回true散劫。 |
下面的接口表示用戶擁有 admin
、menu:edit
權(quán)限中的任意一個(gè)就能能訪問update
方法染簇,如果方法不加@preAuthorize
注解吨岭,意味著所有用戶都需要帶上有效的 token
后能訪問 update
方法
@Log(description = "修改菜單")
@PutMapping(value = "/menus")
@PreAuthorize("hasAnyRole('admin','menu:edit')")
public ResponseEntity update(@Validated @RequestBody Menu resources){
// 略
}
4.4.2 自定義權(quán)限驗(yàn)證
由于每個(gè)接口都需要給超級(jí)管理員放行,而使用 hasAnyRole('admin','user:list')
每次都需要重復(fù)的添加 admin 權(quán)限车荔,因此有自定義權(quán)限驗(yàn)證方式渡冻,在驗(yàn)證的時(shí)候默認(rèn)給擁有admin權(quán)限的用戶放行。
源碼:
package me.zhengjie.config;
import me.zhengjie.utils.SecurityUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 自定義權(quán)限驗(yàn)證:
*
* 由于每個(gè)接口都需要給超級(jí)管理員放行忧便,
* 而使用 hasAnyRole('admin','user:list') 每次都需要重復(fù)的添加 admin 權(quán)限族吻,
* 因此加入了自定義權(quán)限驗(yàn)證方式,
* 在驗(yàn)證的時(shí)候默認(rèn)給擁有admin權(quán)限的用戶放行珠增。
*/
@Service(value = "el")
public class ElPermissionConfig {
public Boolean check(String ...permissions){
// 如果是匿名訪問的超歌,就放行
String anonymous = "anonymous";
if(Arrays.asList(permissions).contains(anonymous)){
return true;
}
// 獲取當(dāng)前用戶的所有權(quán)限
List<String> elPermissions = SecurityUtils.getUserDetails().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 判斷當(dāng)前用戶的所有權(quán)限是否包含接口上定義的權(quán)限
return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
}
使用方式:
@PreAuthorize("@el.check('user:list')")
4.4.3 匿名訪問
在我們使用的時(shí)候,有些接口是不需要驗(yàn)證權(quán)限蒂教,這個(gè)時(shí)候就需要我們給接口放行握础,使用方式如下
1.修改配置文件方式
// 關(guān)鍵代碼,部分略
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 支付寶回調(diào)
.antMatchers("/api/aliPay/return").anonymous()
// 所有請(qǐng)求都需要認(rèn)證
.anyRequest().authenticated();
httpSecurity
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
2.使用注解方式
// 自定義匿名接口
@AnonymousAccess
package com.zhouy.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}
重寫WebSecurityConfigurerAdapter的configure(HttpSecurity httpSecurity)方法
1.權(quán)限驗(yàn)證之前先做jwt驗(yàn)證
//權(quán)限驗(yàn)證之前先JWT驗(yàn)證悴品,驗(yàn)證token有效性禀综、對(duì)等性
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
自定義基于JWT的安全過濾器
package com.zhouy.modules.security.security;
import com.zhouy.modules.security.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import sun.plugin.liveconnect.SecurityContextHelper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* token 校驗(yàn)
*/
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
@Value("${jwt.online}")
private String onlineKey;
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
private final RedisTemplate redisTemplate;
public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsService")UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, RedisTemplate redisTemplate){
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.redisTemplate = redisTemplate;
}
/**
* 這里無論訪問哪個(gè)api都會(huì)進(jìn)到這里简烘,不管帶不帶token
* @param httpServletRequest
* @param httpServletResponse
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authToken = jwtTokenUtil.getToken(httpServletRequest);
OnlineUser onlineUser = null;
try{
onlineUser = (OnlineUser) redisTemplate.opsForValue().get(onlineKey + authToken);
}catch (Exception e){
e.printStackTrace();
}
if (onlineUser!= null & SecurityContextHolder.getContext().getAuthentication() ==null){
JwtUser userDetails = (JwtUser) this.userDetailsService.loadUserByUsername(onlineUser.getUserName());
if (jwtTokenUtil.validateToken(authToken,userDetails))
{
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
權(quán)限驗(yàn)證配置
//權(quán)限驗(yàn)證
httpSecurity
// 禁用 CSRF
// https://blog.csdn.net/xiaoxinshuaiga/article/details/80766369
.csrf().disable()
// 沒通過jwt驗(yàn)證,則執(zhí)行自定義的響應(yīng)處理
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 不創(chuàng)建會(huì)話,要使用jwt托管安全信息定枷,所以把Session禁止掉
/**
* ALWAYS,//總是會(huì)新建一個(gè)Session孤澎。
* NEVER,//不會(huì)新建HttpSession,但是如果有Session存在欠窒,就會(huì)使用它覆旭。
* IF_REQUIRED,//如果有要求的話,會(huì)新建一個(gè)Session岖妄。
* STATELESS;//不會(huì)新建型将,也不會(huì)使用一個(gè)HttpSession。
*/
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 過濾請(qǐng)求
//使用 anonymous() 所有人都能訪問荐虐,但是帶上 token 訪問后會(huì)報(bào)錯(cuò)
.authorizeRequests()
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).anonymous()
// swagger start
//使用 permitAll() 方法所有人都能訪問七兜,包括帶上 token 訪問
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// swagger end
// 文件
.antMatchers("/avatar/**").permitAll()
.antMatchers("/file/**").permitAll()
// 放行OPTIONS請(qǐng)求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/druid/**").permitAll()
// 自定義匿名訪問所有url放行 : 允許 匿名和帶權(quán)限以及登錄用戶訪問
.antMatchers(anonymousUrls.toArray(new String[0])).permitAll()
// 所有請(qǐng)求都需要認(rèn)證
.anyRequest().authenticated()
// 防止iframe 造成跨域
.and().headers().frameOptions().disable();
授權(quán)異常處理( 沒通過jwt驗(yàn)證,則執(zhí)行自定義的響應(yīng)處理)
package com.zhouy.modules.security.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint,Serializable {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 當(dāng)用戶嘗試訪問安全的REST資源而不提供任何憑據(jù)時(shí)福扬,將調(diào)用此方法發(fā)送401 響應(yīng)
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e==null?"Unauthorized":e.getMessage());
}
}
5.JWT生成token
5.1 簡(jiǎn)介
JSON Web Token(JWT)是目前分布式系統(tǒng)中最流行的跨域身份驗(yàn)證解決方案腕铸。
5.2 跨域身份驗(yàn)證
Internet服務(wù)無法與用戶身份驗(yàn)證分開。一般過程如下铛碑。
1.用戶向服務(wù)器發(fā)送用戶名和密碼狠裹。
2.驗(yàn)證服務(wù)器后,相關(guān)數(shù)據(jù)(如用戶角色汽烦,登錄時(shí)間等)將保存在當(dāng)前會(huì)話中涛菠。
3.服務(wù)器向用戶返回session_id,session信息都會(huì)寫入到用戶的Cookie撇吞。
4.用戶的每個(gè)后續(xù)請(qǐng)求都將通過在Cookie中取出session_id傳給服務(wù)器碗暗。
5.服務(wù)器收到session_id并對(duì)比之前保存的數(shù)據(jù),確認(rèn)用戶的身份梢夯。
這種模式最大的問題是言疗,沒有分布式架構(gòu),無法支持橫向擴(kuò)展颂砸。如果使用一個(gè)服務(wù)器噪奄,該模式完全沒有問題。但是人乓,如果它是服務(wù)器群集或面向服務(wù)的跨域體系結(jié)構(gòu)的話勤篮,則需要一個(gè)統(tǒng)一的session數(shù)據(jù)庫(kù)庫(kù)來保存會(huì)話數(shù)據(jù)實(shí)現(xiàn)共享,這樣負(fù)載均衡下的每個(gè)服務(wù)器才可以正確的驗(yàn)證用戶身份色罚。
例如一個(gè)實(shí)際中常見的單點(diǎn)登陸的需求:站點(diǎn)A和站點(diǎn)B提供同一公司的相關(guān)服務(wù)∨龅蓿現(xiàn)在要求用戶只需要登錄其中一個(gè)網(wǎng)站,然后它就會(huì)自動(dòng)登錄到另一個(gè)網(wǎng)站戳护。怎么做金抡?
一種解決方案是聽過持久化session數(shù)據(jù)瀑焦,寫入數(shù)據(jù)庫(kù)或文件持久層等。收到請(qǐng)求后梗肝,驗(yàn)證服務(wù)從持久層請(qǐng)求數(shù)據(jù)榛瓮。該解決方案的優(yōu)點(diǎn)在于架構(gòu)清晰,而缺點(diǎn)是架構(gòu)修改比較費(fèi)勁巫击,整個(gè)服務(wù)的驗(yàn)證邏輯層都需要重寫禀晓,工作量相對(duì)較大。而且由于依賴于持久層的數(shù)據(jù)庫(kù)或者問題系統(tǒng)坝锰,會(huì)有單點(diǎn)風(fēng)險(xiǎn)粹懒,如果持久層失敗,整個(gè)認(rèn)證體系都會(huì)掛掉顷级。
另外一種靈活的解決方案凫乖,通過客戶端保存數(shù)據(jù),而服務(wù)器根本不保存會(huì)話數(shù)據(jù)愕把,每個(gè)請(qǐng)求都被發(fā)送回服務(wù)器。 JWT是這種解決方案的代表森爽。
5.3 JWT原則
JWT的原則是在服務(wù)器身份驗(yàn)證之后恨豁,將生成一個(gè)JSON對(duì)象并將其發(fā)送回用戶,如下所示爬迟。
{
"UserName": "Chongchong",
"Role": "Admin",
"Expire": "2018-08-08 20:15:56"
}
之后橘蜜,當(dāng)用戶與服務(wù)器通信時(shí),客戶在請(qǐng)求中發(fā)回JSON對(duì)象付呕。服務(wù)器僅依賴于這個(gè)JSON對(duì)象來標(biāo)識(shí)用戶计福。為了防止用戶篡改數(shù)據(jù),服務(wù)器將在生成對(duì)象時(shí)添加簽名(有關(guān)詳細(xì)信息徽职,請(qǐng)參閱下文)象颖。
服務(wù)器不保存任何會(huì)話數(shù)據(jù),即服務(wù)器變?yōu)闊o狀態(tài)姆钉,使其更容易擴(kuò)展说订。
5.4 JWT數(shù)據(jù)結(jié)構(gòu)
典型的,一個(gè)JWT看起來如下圖潮瓶。
改對(duì)象為一個(gè)很長(zhǎng)的字符串陶冷,字符之間通過"."分隔符分為三個(gè)子串。注意JWT對(duì)象為一個(gè)長(zhǎng)字串毯辅,各字串之間也沒有換行符埂伦,此處為了演示需要,我們特意分行并用不同顏色表示了思恐。每一個(gè)子串表示了一個(gè)功能塊沾谜,總共有以下三個(gè)部分:
JWT的三個(gè)部分如下膊毁。JWT頭、有效載荷和簽名类早,將它們寫成一行如下媚媒。
我們將在下面介紹這三個(gè)部分。
5.5 JWT頭
JWT頭部分是一個(gè)描述JWT元數(shù)據(jù)的JSON對(duì)象涩僻,通常如下所示缭召。
{
"alg": "HS256",
"typ": "JWT"
}
在上面的代碼中,alg屬性表示簽名使用的算法逆日,默認(rèn)為HMAC SHA256(寫為HS256)嵌巷;typ屬性表示令牌的類型,JWT令牌統(tǒng)一寫為JWT室抽。
最后搪哪,使用Base64 URL算法將上述JSON對(duì)象轉(zhuǎn)換為字符串保存。
5.6 有效載荷
有效載荷部分坪圾,是JWT的主體內(nèi)容部分晓折,也是一個(gè)JSON對(duì)象,包含需要傳遞的數(shù)據(jù)兽泄。 JWT指定七個(gè)默認(rèn)字段供選擇漓概。
iss:發(fā)行人
exp:到期時(shí)間
sub:主題
aud:用戶
nbf:在此之前不可用
iat:發(fā)布時(shí)間
jti:JWT ID用于標(biāo)識(shí)該JWT
除以上默認(rèn)字段外,我們還可以自定義私有字段病梢,如下例:
{
"sub": "1234567890",
"name": "chongchong",
"admin": true
}
請(qǐng)注意胃珍,默認(rèn)情況下JWT是未加密的,任何人都可以解讀其內(nèi)容蜓陌,因此不要構(gòu)建隱私信息字段觅彰,存放保密信息,以防止信息泄露钮热。
JSON對(duì)象也使用Base64 URL算法轉(zhuǎn)換為字符串保存填抬。
5..7 簽名哈希
簽名哈希部分是對(duì)上面兩部分?jǐn)?shù)據(jù)簽名,通過指定的算法生成哈希隧期,以確保數(shù)據(jù)不會(huì)被篡改痴奏。
首先,需要指定一個(gè)密碼(secret)厌秒。該密碼僅僅為保存在服務(wù)器中读拆,并且不能向用戶公開。然后鸵闪,使用標(biāo)頭中指定的簽名算法(默認(rèn)情況下為HMAC SHA256)根據(jù)以下公式生成簽名檐晕。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret)
在計(jì)算出簽名哈希后,JWT頭,有效載荷和簽名哈希的三個(gè)部分組合成一個(gè)字符串辟灰,每個(gè)部分用"."分隔个榕,就構(gòu)成整個(gè)JWT對(duì)象。
5.8 Base64URL算法
如前所述芥喇,JWT頭和有效載荷序列化的算法都用到了Base64URL西采。該算法和常見Base64算法類似,稍有差別继控。
作為令牌的JWT可以放在URL中(例如api.example/?token=xxx)械馆。 Base64中用的三個(gè)字符是"+","/"和"="武通,由于在URL中有特殊含義霹崎,因此Base64URL中對(duì)他們做了替換:"="去掉,"+"用"-"替換冶忱,"/"用"_"替換尾菇,這就是Base64URL算法,很簡(jiǎn)單把囚枪。
5.9. JWT的用法
客戶端接收服務(wù)器返回的JWT派诬,將其存儲(chǔ)在Cookie或localStorage中。
此后链沼,客戶端將在與服務(wù)器交互中都會(huì)帶JWT默赂。如果將它存儲(chǔ)在Cookie中,就可以自動(dòng)發(fā)送忆植,但是不會(huì)跨域放可,因此一般是將它放入HTTP請(qǐng)求的Header Authorization字段中谒臼。
Authorization: Bearer
當(dāng)跨域時(shí)朝刊,也可以將JWT被放置于POST請(qǐng)求的數(shù)據(jù)主體中。
5.10 JWT問題和趨勢(shì)
1蜈缤、JWT默認(rèn)不加密拾氓,但可以加密。生成原始令牌后底哥,可以使用改令牌再次對(duì)其進(jìn)行加密咙鞍。
2、當(dāng)JWT未加密方法是趾徽,一些私密數(shù)據(jù)無法通過JWT傳輸续滋。
3、JWT不僅可用于認(rèn)證孵奶,還可用于信息交換疲酌。善用JWT有助于減少服務(wù)器請(qǐng)求數(shù)據(jù)庫(kù)的次數(shù)。
4、JWT的最大缺點(diǎn)是服務(wù)器不保存會(huì)話狀態(tài)朗恳,所以在使用期間不可能取消令牌或更改令牌的權(quán)限湿颅。也就是說,一旦JWT簽發(fā)粥诫,在有效期內(nèi)將會(huì)一直有效油航。
5、JWT本身包含認(rèn)證信息怀浆,因此一旦信息泄露谊囚,任何人都可以獲得令牌的所有權(quán)限。為了減少盜用揉稚,JWT的有效期不宜設(shè)置太長(zhǎng)秒啦。對(duì)于某些重要操作,用戶在使用時(shí)應(yīng)該每次都進(jìn)行進(jìn)行身份驗(yàn)證搀玖。
6余境、為了減少盜用和竊取,JWT不建議使用HTTP協(xié)議來傳輸代碼灌诅,而是使用加密的HTTPS協(xié)議進(jìn)行傳輸芳来。
5.11 JWT工具類
package com.zhouy.modules.security.utils;
import com.zhouy.modules.security.security.JwtUser;
import com.zhouy.modules.system.domain.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
private Clock clock = DefaultClock.INSTANCE;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.header}")
private String tokenHeader;
// 生成token
public String generateToken(JwtUser user){
Key key = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS512.getJcaName());
final Date createdDate = clock.now();
final Date expirationDate = new Date(createdDate.getTime()+expiration);
return Jwts.builder()
.setClaims(Jwts.claims())
.setSubject(user.getUsername())
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, key)
.compact();
}
// 獲取token
public String getToken(HttpServletRequest request){
final String requestHeader = request.getHeader(tokenHeader);
if (requestHeader != null && requestHeader.startsWith("Bearer ")){
return requestHeader.substring(7);
}
return null;
}
// 獲取token創(chuàng)建日期
private Date getIssuedAtDateFromToken(String token){
return getClaimFromToken(token,Claims::getIssuedAt);
}
private <T> T getClaimFromToken(String token,Function<Claims,T> claimsResolver){
Key key = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS512.getJcaName());
final Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
return claimsResolver.apply(claims);
}
private Boolean isTokenExpired(String token) {
final Date expiration = getClaimFromToken(token, Claims::getExpiration);;
return expiration.before(clock.now());
}
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser jwtUser = (JwtUser) userDetails;
final Date created = getIssuedAtDateFromToken(token);
// 如果token存在,且token創(chuàng)建日期 > 最后修改密碼的日期 則代表token有效
return (!isTokenExpired(token)
&& !isCreatedBeforeLastPasswordReset(created,jwtUser.getLastPasswordResetDate()));
}
}
6. spring-security與JWT引入
<!-- starter boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/>
</parent>
<!-- starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
7.jwt的yaml配置
#jwt
jwt:
header: Authorization
secret: mySecret
# token 過期時(shí)間/毫秒猜拾,6小時(shí) 1小時(shí) = 3600000 毫秒
expiration: 21600000
# 在線用戶key
online: online-token
# 驗(yàn)證碼
codeKey: code-key
8.前后端分離跨域處理
package com.zhouy.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer{
/**
* 全局跨越配置即舌,核心配置。
* 用于前后端分離挎袜,前端能成功調(diào)用后端api
*
* 2020-04-14 23:33:00
* 前天晚上和今天晚上一直卡在這個(gè)地方顽聂,一直以為spring-security沒有配置正確,
* 可怎么配置都不行盯仪,其實(shí)現(xiàn)在想起來是我自己理解錯(cuò)了紊搪,一開始前端調(diào)用獲取驗(yàn)證碼api,
* 而該api我是在spring-security配置中做了放行的全景,可前端一直報(bào)跨域請(qǐng)求錯(cuò)誤耀石,
* 我就認(rèn)為沒有spring-security沒有配置禁止跨域,因?yàn)橐坏┛缬蚰敲磗pring-security的鑒權(quán)和認(rèn)證都失效爸黄,
* 這么理解是完全錯(cuò)誤的滞伟,即使允許了跨域spring-security還是會(huì)去認(rèn)證和鑒權(quán)的,但是不允許跨域炕贵,
* 那前端根本就進(jìn)不了后臺(tái)梆奈,還談何認(rèn)證和授權(quán)。
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedHeaders("*")
.allowedOrigins("*")
.allowedMethods("GET","POST","DELETE");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0);
}
}