spring-security+jwt實(shí)戰(zhàn)

權(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)系,如下圖

rbac.png

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)證過程

authentication.png

重寫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)證。

remeberme.png

上述的用戶認(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散劫。

下面的接口表示用戶擁有 adminmenu: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)用戶的身份梢夯。

session.png

這種模式最大的問題是言疗,沒有分布式架構(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ì)掛掉顷级。

s2.png

另外一種靈活的解決方案凫乖,通過客戶端保存數(shù)據(jù),而服務(wù)器根本不保存會(huì)話數(shù)據(jù)愕把,每個(gè)請(qǐng)求都被發(fā)送回服務(wù)器。 JWT是這種解決方案的代表森爽。

s3.png

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頭、有效載荷和簽名类早,將它們寫成一行如下媚媒。

img

我們將在下面介紹這三個(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);
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末称开,一起剝皮案震驚了整個(gè)濱河市亩钟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖径荔,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件督禽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡总处,警方通過查閱死者的電腦和手機(jī)狈惫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹦马,“玉大人胧谈,你說我怎么就攤上這事≥┢担” “怎么了菱肖?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)旭从。 經(jīng)常有香客問我稳强,道長(zhǎng),這世上最難降的妖魔是什么和悦? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任退疫,我火速辦了婚禮,結(jié)果婚禮上鸽素,老公的妹妹穿的比我還像新娘褒繁。我一直安慰自己,他們只是感情好馍忽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布棒坏。 她就那樣靜靜地躺著,像睡著了一般遭笋。 火紅的嫁衣襯著肌膚如雪坝冕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天坐梯,我揣著相機(jī)與錄音徽诲,去河邊找鬼刹帕。 笑死吵血,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的偷溺。 我是一名探鬼主播蹋辅,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼挫掏!你這毒婦竟也來了侦另?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎褒傅,沒想到半個(gè)月后弃锐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡殿托,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年霹菊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片支竹。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡旋廷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出礼搁,到底是詐尸還是另有隱情饶碘,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布馒吴,位于F島的核電站扎运,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏饮戳。R本人自食惡果不足惜绪囱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望莹捡。 院中可真熱鬧鬼吵,春花似錦、人聲如沸篮赢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽启泣。三九已至涣脚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寥茫,已是汗流浹背遣蚀。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纱耻,地道東北人芭梯。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像弄喘,于是被迫代替她去往敵國(guó)和親玖喘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容