欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring Security+JWT實(shí)現(xiàn)認(rèn)證與授權(quán)的實(shí)現(xiàn)

 更新時(shí)間:2022年04月27日 09:44:17   作者:失憶機(jī)器  
本文主要介紹了Spring Security+JWT實(shí)現(xiàn)認(rèn)證與授權(quán)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

認(rèn)證:驗(yàn)證當(dāng)前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認(rèn)具體是哪個(gè)用戶

授權(quán):經(jīng)過認(rèn)證后判斷當(dāng)前用戶是否有權(quán)限進(jìn)行某個(gè)操作

一、登錄校驗(yàn)流程

1、Spring Security 完整流程

SpringSecurity的原理其實(shí)就是一個(gè)過濾器鏈,內(nèi)部包含了提供各種功能的過濾器。部分核心過濾器如下圖:

UsernamePasswordAuthenticationFilter:負(fù)責(zé)處理在登錄頁填寫了用戶名密碼后的登陸請求。

ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException(訪問出錯(cuò))和AuthenticationExcption(認(rèn)證出錯(cuò))。

FilterSecurityInterceptor:負(fù)責(zé)權(quán)限校驗(yàn)的過濾器。

2、Spring Security的默認(rèn)登陸驗(yàn)證流程。

Authentication接口:它的實(shí)現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。

AuthenticationManager接口:定義了認(rèn)證Authentication的方法。

UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的方法。

UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。

3、 整合JWT大致流程

登錄

        ①自定義登錄接口

                調(diào)用ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過生成JWT。

                把用戶信息存入redis中        

        ②自定義UserDetailsService

                在這個(gè)實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫

校驗(yàn)

        ①定義Jwt認(rèn)證過濾器

        獲取token

        解析token獲取其中的userid

        從redis中獲取用戶信息

        存入SecurityContextHolder

Redis使用Fastjson序列化

<!-- spring data redis 依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 對象池依賴 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<!-- JSON工具 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.76</version>
</dependency>
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
 
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
 
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
 
    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
 
    static {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }
 
    private final Class<T> clazz;
 
    public FastJson2JsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }
 
    /**
     * 序列化
     */
    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (null == t) {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }
 
    /**
     * 反序列化
     */
    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (null == bytes || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return (T) JSON.parseObject(str, clazz);
    }
}
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisCacheAutoConfiguration {
 
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        FastJson2JsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
 
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
 
        // value序列化方式采用fastJson
        template.setValueSerializer(fastJsonRedisSerializer);
        // hash的value序列化方式采用fastJson
        template.setHashValueSerializer(fastJsonRedisSerializer);
 
        template.afterPropertiesSet();
        return template;
    }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
 
import java.util.*;
import java.util.concurrent.TimeUnit;
 
/**
 * spring redis 工具類
 **/
@Component
public class RedisUtil {
 
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
 
    /**
     * 緩存基本的對象,Integer、String、實(shí)體類等
     *
     * @param key   緩存的鍵值
     * @param value 緩存的值
     * @return 緩存的對象
     */
    public ValueOperations<Object, Object> setCacheObject(Object key, Object value) {
        ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
        operation.set(key, value);
        return operation;
    }
 
    /**
     * 緩存基本的對象,Integer、String、實(shí)體類等
     *
     * @param key      緩存的鍵值
     * @param value    緩存的值
     * @param timeout  時(shí)間
     * @param timeUnit 時(shí)間顆粒度
     * @return 緩存的對象
     */
    public ValueOperations<Object, Object> setCacheObject(Object key, Object value, Integer timeout, TimeUnit timeUnit) {
        ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
        operation.set(key, value, timeout, timeUnit);
        return operation;
    }
 
    /**
     * 獲得緩存的基本對象。
     *
     * @param key 緩存鍵值
     * @return 緩存鍵值對應(yīng)的數(shù)據(jù)
     */
    public Object getCacheObject(Object key) {
        ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }
 
    /**
     * 刪除單個(gè)對象
     *
     * @param key
     */
    public void deleteObject(Object key) {
        redisTemplate.delete(key);
    }
 
    /**
     * 刪除集合對象
     *
     * @param collection
     */
    public void deleteObject(Collection collection) {
        redisTemplate.delete(collection);
    }
 
    public Long getExpire(String key) {
        return redisTemplate.getExpire(key);
    }
 
    public void expire(String key, int expire, TimeUnit timeUnit) {
        redisTemplate.expire(key, expire, timeUnit);
    }
 
    /**
     * 緩存List數(shù)據(jù)
     *
     * @param key      緩存的鍵值
     * @param dataList 待緩存的List數(shù)據(jù)
     * @return 緩存的對象
     */
    public ListOperations<Object, Object> setCacheList(Object key, List<Object> dataList) {
        ListOperations listOperation = redisTemplate.opsForList();
        if (null != dataList) {
            int size = dataList.size();
            for (Object o : dataList) {
                listOperation.leftPush(key, o);
            }
        }
        return listOperation;
    }
 
    /**
     * 獲得緩存的list對象
     *
     * @param key 緩存的鍵值
     * @return 緩存鍵值對應(yīng)的數(shù)據(jù)
     */
    public List<Object> getCacheList(String key) {
        List<Object> dataList = new ArrayList<>();
        ListOperations<Object, Object> listOperation = redisTemplate.opsForList();
        Long size = listOperation.size(key);
        if (null != size) {
            for (int i = 0; i < size; i++) {
                dataList.add(listOperation.index(key, i));
            }
        }
        return dataList;
    }
 
    /**
     * 緩存Set
     *
     * @param key     緩存鍵值
     * @param dataSet 緩存的數(shù)據(jù)
     * @return 緩存數(shù)據(jù)的對象
     */
    public BoundSetOperations<Object, Object> setCacheSet(String key, Set<Object> dataSet) {
        BoundSetOperations<Object, Object> setOperation = redisTemplate.boundSetOps(key);
        for (Object o : dataSet) {
            setOperation.add(o);
        }
        return setOperation;
    }
 
    /**
     * 獲得緩存的set
     *
     * @param key
     * @return
     */
    public Set<Object> getCacheSet(Object key) {
        Set<Object> dataSet = new HashSet<>();
        BoundSetOperations<Object, Object> operation = redisTemplate.boundSetOps(key);
        dataSet = operation.members();
        return dataSet;
    }
 
    /**
     * 緩存Map
     *
     * @param key
     * @param dataMap
     * @return
     */
    public HashOperations<Object, Object, Object> setCacheMap(Object key, Map<Object, Object> dataMap) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        if (null != dataMap) {
            for (Map.Entry<Object, Object> entry : dataMap.entrySet()) {
                hashOperations.put(key, entry.getKey(), entry.getValue());
            }
        }
        return hashOperations;
    }
 
    /**
     * 獲得緩存的Map
     *
     * @param key
     * @return
     */
    public Map<Object, Object> getCacheMap(Object key) {
        Map<Object, Object> map = redisTemplate.opsForHash().entries(key);
        return map;
    }
 
    /**
     * 獲得緩存的基本對象列表
     *
     * @param pattern 字符串前綴
     * @return 對象列表
     */
    public Collection<Object> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }
}

前端響應(yīng)類

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
    /**
     * 狀態(tài)碼
     */
    private Integer code;
    /**
     * 提示信息,如果有錯(cuò)誤時(shí),前端可以獲取該字段進(jìn)行提示
     */
    private String msg;
    /**
     * 查詢到的結(jié)果數(shù)據(jù),
     */
    private T data;
 
    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
 
    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }
 
    public Integer getCode() {
        return code;
    }
 
    public void setCode(Integer code) {
        this.code = code;
    }
 
    public String getMsg() {
        return msg;
    }
 
    public void setMsg(String msg) {
        this.msg = msg;
    }
 
    public T getData() {
        return data;
    }
 
    public void setData(T data) {
        this.data = data;
    }
 
    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

JWT工具類

public class JwtUtil {
 
    //有效期為
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一個(gè)小時(shí)
    //設(shè)置秘鑰明文
    public static final String JWT_KEY = "zhangao";
 
    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * 生成jtw
     * @param subject token中要存放的數(shù)據(jù)(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 設(shè)置過期時(shí)間
        return builder.compact();
    }
 
    /**
     * 生成jtw
     * @param subject token中要存放的數(shù)據(jù)(json格式)
     * @param ttlMillis token超時(shí)時(shí)間
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設(shè)置過期時(shí)間
        return builder.compact();
    }
 
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主題  可以是JSON數(shù)據(jù)
                .setIssuer("sg")     // 簽發(fā)者
                .setIssuedAt(now)      // 簽發(fā)時(shí)間
                .signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個(gè)參數(shù)為秘鑰
                .setExpiration(expDate);
    }
 
    /**
     * 創(chuàng)建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 設(shè)置過期時(shí)間
        return builder.compact();
    }
 
    public static void main(String[] args) throws Exception {
//        String jwt = createJWT("2123");
        Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");
        String subject = claims.getSubject();
        System.out.println(subject);
//        System.out.println(claims);
    }
 
    /**
     * 生成加密后的秘鑰 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
 
 
}

創(chuàng)建數(shù)據(jù)庫表信息和實(shí)體,配置數(shù)據(jù)庫連接信息

定義mapper等一系列接口。xml等。用mybatis-plus方便一點(diǎn),注意Mapper繼承BaseMapper<實(shí)體類>,實(shí)體類中需要加@TableName(value = "表名") ,id字段上加 @TableId

在application.yml中配置mapperXML文件的位置

引入依賴

<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

重寫UserDetailsService的方法

創(chuàng)建一個(gè)類實(shí)現(xiàn)UserDetailsService接口,重寫其中的方法。從數(shù)據(jù)庫中查詢用戶信息,進(jìn)行校驗(yàn)。(如果沒有重寫的話,就是上面說的spring security默認(rèn)的使用UserDetailsService接口下面的InMemoryUserDetailsManager實(shí)現(xiàn)類中的方法,是在內(nèi)存中查找。這個(gè)是需要根據(jù)我們具體的系統(tǒng)來重寫的。)

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
 
    @Autowired
    private UserMapper userMapper;
 
    @Autowired
    private MenuMapper menuMapper;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 
        //查詢用戶信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //如果沒有查詢到用戶就拋出異常
        if(Objects.isNull(user)){
            throw new RuntimeException("用戶名或者密碼錯(cuò)誤");
        }
 
//      查詢權(quán)限
        List<String> list = menuMapper.selectPermsByUserId(user.getId());
        //把數(shù)據(jù)封裝成UserDetails返回
        return new LoginUser(user,list);
    }
}

因?yàn)閁serDetailsService方法的返回值是UserDetails類型,所以需要定義一個(gè)類,實(shí)現(xiàn)該接口,把用戶信息封裝在其中。

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
 
    private User user;
 
    private List<String> permissions;
 
    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permissions中String類型的權(quán)限信息封裝成SimpleGrantedAuthority對象
//       authorities = new ArrayList<>();
//        for (String permission : permissions) {
//            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
//            authorities.add(authority);
//        }
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getUserName();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
}

重寫登錄接口

        接下我們需要自定義登陸接口,然后讓SpringSecurity對這個(gè)接口放行,讓用戶訪問這個(gè)接口的時(shí)候不用登錄也能訪問。

在接口中我們通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

認(rèn)證成功的話要生成一個(gè)jwt,放入響應(yīng)中返回。并且為了讓用戶下回請求時(shí)能通過jwt識別出具體

的是哪個(gè)用戶,我們需要把用戶信息存入redis,可以把用戶id作為key。

@RestController
public class LoginController {
 
    @Autowired
    private LoginServcie loginServcie;
 
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        //登錄
        return loginServcie.login(user);
    }
 
    @RequestMapping("/user/logout")
    public ResponseResult logout(){
        return loginServcie.logout();
    }
}
@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous()// .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證 .anyRequest().authenticated(); //添加過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置異常處理器 http.exceptionHandling() //配置認(rèn)證失敗處理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允許跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } 
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    //創(chuàng)建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
 
 
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
 
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //關(guān)閉csrf
                .csrf().disable()
                //不通過Session獲取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 對于登錄接口 允許匿名訪問
                .antMatchers("/user/login").anonymous()
//                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證
                .anyRequest().authenticated();
 
        //添加過濾器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
 
        //配置異常處理器
        http.exceptionHandling()
                //配置認(rèn)證失敗處理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
 
        //允許跨域
        http.cors();
    }
 
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
@Service
public class LoginServiceImpl implements LoginServcie {
 
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @Autowired
    private RedisCache redisCache;
 
    @Override
    public ResponseResult login(User user) {
        //AuthenticationManager authenticate進(jìn)行用戶認(rèn)證
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果認(rèn)證沒通過,給出對應(yīng)的提示
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登錄失敗");
        }
        //如果認(rèn)證通過了,使用userid生成一個(gè)jwt jwt存入ResponseResult返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userid = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userid);
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        //把完整的用戶信息存入redis  userid作為key
        redisCache.setCacheObject("login:"+userid,loginUser);
        return new ResponseResult(200,"登錄成功",map);
    }
 
    @Override
    public ResponseResult logout() {
        //獲取SecurityContextHolder中的用戶id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        //刪除redis中的值
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"注銷成功");
    }
}

認(rèn)證過濾器

我們需要自定義一個(gè)過濾器,這個(gè)過濾器會去獲取請求頭中的token,對token進(jìn)行解析取出其中的userid。(把這個(gè)放到最前面,放到UsernamePassword的那個(gè)前面)這樣做就是為了除了登錄的時(shí)候去查詢數(shù)據(jù)庫外,其他時(shí)候都用JWT配合Redis進(jìn)行認(rèn)證。

使用userid去redis中獲取對應(yīng)的LoginUser對象。

然后封裝Authentication對象存入SecurityContextHolder

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 
    @Autowired
    private RedisCache redisCache;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //獲取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //從redis中獲取用戶信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用戶未登錄");
        }
        //存入SecurityContextHolder
        //TODO 獲取權(quán)限信息封裝到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

退出登陸

        我們只需要定義一個(gè)登陸接口,然后獲取SecurityContextHolder中的認(rèn)證信息,刪除redis中對應(yīng)的數(shù)據(jù)即可。

 @Override
    public ResponseResult logout() {
        //獲取SecurityContextHolder中的用戶id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        //刪除redis中的值
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"注銷成功");
    }

授權(quán)基本流程

        在SpringSecurity中,會使用默認(rèn)的FilterSecurityInterceptor來進(jìn)行權(quán)限校驗(yàn)。在FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權(quán)限信息。當(dāng)前用戶是否擁有訪問當(dāng)前資源所需的權(quán)限。

所以我們在項(xiàng)目中只需要把當(dāng)前登錄用戶的權(quán)限信息也存入Authentication。

然后設(shè)置我們的資源所需要的權(quán)限即可。

限制訪問資源所需權(quán)限

        SpringSecurity為我們提供了基于注解的權(quán)限控制方案,這也是我們項(xiàng)目中主要采用的方式。我們可以使用注解去指定訪問對應(yīng)的資源所需的權(quán)限。

        但是要使用它我們需要先開啟相關(guān)配置。

        然后就可以使用對應(yīng)的注解。@PreAuthorize  

@RestController
public class HelloController {
 
    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello(){
        return "hello";
    }
}

封裝權(quán)限信息

我們前面在寫UserDetailsServiceImpl的時(shí)候說過,在查詢出用戶后還要獲取對應(yīng)的權(quán)限信息,封裝到UserDetails中返回。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
 
    @Autowired
    private UserMapper userMapper;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        if(Objects.isNull(user)){
            throw new RuntimeException("用戶名或密碼錯(cuò)誤");
        }
        //TODO 根據(jù)用戶查詢權(quán)限信息 添加到LoginUser中
        List<String> list = new ArrayList<>(Arrays.asList("test"));
        return new LoginUser(user,list);
    }
}

RBAC權(quán)限模型

RBAC權(quán)限模型(Role-Based Access Control)即:基于角色的權(quán)限控制。這是目前最常被開發(fā)者使用也是相對易用、通用權(quán)限模型。

 參考表:

CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
 
USE `sg_security`;
 
/*Table structure for table `sys_menu` */
 
DROP TABLE IF EXISTS `sys_menu`;
 
CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜單名',
  `path` varchar(200) DEFAULT NULL COMMENT '路由地址',
  `component` varchar(255) DEFAULT NULL COMMENT '組件路徑',
  `visible` char(1) DEFAULT '0' COMMENT '菜單狀態(tài)(0顯示 1隱藏)',
  `status` char(1) DEFAULT '0' COMMENT '菜單狀態(tài)(0正常 1停用)',
  `perms` varchar(100) DEFAULT NULL COMMENT '權(quán)限標(biāo)識',
  `icon` varchar(100) DEFAULT '#' COMMENT '菜單圖標(biāo)',
  `create_by` bigint(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_by` bigint(20) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `del_flag` int(11) DEFAULT '0' COMMENT '是否刪除(0未刪除 1已刪除)',
  `remark` varchar(500) DEFAULT NULL COMMENT '備注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜單表';
 
/*Table structure for table `sys_role` */
 
DROP TABLE IF EXISTS `sys_role`;
 
CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL,
  `role_key` varchar(100) DEFAULT NULL COMMENT '角色權(quán)限字符串',
  `status` char(1) DEFAULT '0' COMMENT '角色狀態(tài)(0正常 1停用)',
  `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
  `create_by` bigint(200) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_by` bigint(200) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `remark` varchar(500) DEFAULT NULL COMMENT '備注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
 
/*Table structure for table `sys_role_menu` */
 
DROP TABLE IF EXISTS `sys_role_menu`;
 
CREATE TABLE `sys_role_menu` (
  `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色I(xiàn)D',
  `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜單id',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
 
/*Table structure for table `sys_user` */
 
DROP TABLE IF EXISTS `sys_user`;
 
CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用戶名',
  `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵稱',
  `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密碼',
  `status` char(1) DEFAULT '0' COMMENT '賬號狀態(tài)(0正常 1停用)',
  `email` varchar(64) DEFAULT NULL COMMENT '郵箱',
  `phonenumber` varchar(32) DEFAULT NULL COMMENT '手機(jī)號',
  `sex` char(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)',
  `avatar` varchar(128) DEFAULT NULL COMMENT '頭像',
  `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用戶類型(0管理員,1普通用戶)',
  `create_by` bigint(20) DEFAULT NULL COMMENT '創(chuàng)建人的用戶id',
  `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新時(shí)間',
  `del_flag` int(11) DEFAULT '0' COMMENT '刪除標(biāo)志(0代表未刪除,1代表已刪除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';
 
/*Table structure for table `sys_user_role` */
 
DROP TABLE IF EXISTS `sys_user_role`;
 
CREATE TABLE `sys_user_role` (
  `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用戶id',
  `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

查詢條件

SELECT 
	DISTINCT m.`perms`
FROM
	sys_user_role ur
	LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
	LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
	LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
	user_id = 2
	AND r.`status` = 0
	AND m.`status` = 0
/**
 * 菜單表(Menu)實(shí)體類
 *
 * @author makejava
 * @since 2021-11-24 15:30:08
 */
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
    private static final long serialVersionUID = -54979041104113736L;
    
        @TableId
    private Long id;
    /**
    * 菜單名
    */
    private String menuName;
    /**
    * 路由地址
    */
    private String path;
    /**
    * 組件路徑
    */
    private String component;
    /**
    * 菜單狀態(tài)(0顯示 1隱藏)
    */
    private String visible;
    /**
    * 菜單狀態(tài)(0正常 1停用)
    */
    private String status;
    /**
    * 權(quán)限標(biāo)識
    */
    private String perms;
    /**
    * 菜單圖標(biāo)
    */
    private String icon;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    /**
    * 是否刪除(0未刪除 1已刪除)
    */
    private Integer delFlag;
    /**
    * 備注
    */
    private String remark;
}
public interface MenuMapper extends BaseMapper<Menu> {
    List<String> selectPermsByUserId(Long id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sangeng.mapper.MenuMapper">
 
 
    <select id="selectPermsByUserId" resultType="java.lang.String">
        SELECT
            DISTINCT m.`perms`
        FROM
            sys_user_role ur
            LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
            LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
            LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            user_id = #{userid}
            AND r.`status` = 0
            AND m.`status` = 0
    </select>
</mapper>
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml 

自定義失敗處理

我們還希望在認(rèn)證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對響應(yīng)進(jìn)行統(tǒng)一的處理。要實(shí)現(xiàn)這個(gè)功能我們需要知道SpringSecurity的異常處理機(jī)制。

在SpringSecurity中,如果我們在認(rèn)證或者授權(quán)的過程中出現(xiàn)了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。

如果是認(rèn)證過程中出現(xiàn)的異常會被封裝成AuthenticationException然后調(diào)用AuthenticationEntryPoint對象的方法去進(jìn)行異常處理。

如果是授權(quán)過程中出現(xiàn)的異常會被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對象的方法去進(jìn)行異常處理。

所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置給SpringSecurity即可。

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "權(quán)限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
 
    }
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "認(rèn)證失敗請重新登錄");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    }
}
public class WebUtils
{
    /**
     * 將字符串渲染到客戶端
     * 
     * @param response 渲染對象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}

配置給SpringSecurity

到此這篇關(guān)于Spring Security+JWT實(shí)現(xiàn)認(rèn)證與授權(quán)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Spring Security JWT認(rèn)證與授權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java中的Vector和ArrayList區(qū)別及比較

    Java中的Vector和ArrayList區(qū)別及比較

    這篇文章主要介紹了Java中的Vector和ArrayList區(qū)別及比較,本文從API、同步、數(shù)據(jù)增長、使用模式4個(gè)方面總結(jié)了它們之間的不同之處,需要的朋友可以參考下
    2015-03-03
  • JAVA學(xué)習(xí)筆記:注釋、變量的聲明和定義操作實(shí)例分析

    JAVA學(xué)習(xí)筆記:注釋、變量的聲明和定義操作實(shí)例分析

    這篇文章主要介紹了JAVA學(xué)習(xí)筆記:注釋、變量的聲明和定義操作,結(jié)合實(shí)例形式分析了Java注釋、變量的聲明和定義相關(guān)原理、實(shí)現(xiàn)方法及操作注意事項(xiàng),需要的朋友可以參考下
    2020-04-04
  • Java復(fù)制文件常用的三種方法

    Java復(fù)制文件常用的三種方法

    今天小編就為大家分享一篇關(guān)于Java復(fù)制文件常用的三種方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧
    2019-03-03
  • quarzt定時(shí)調(diào)度任務(wù)解析

    quarzt定時(shí)調(diào)度任務(wù)解析

    這篇文章主要介紹了quarzt定時(shí)調(diào)度任務(wù),具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-12-12
  • Spring初始化和銷毀的實(shí)現(xiàn)方法

    Spring初始化和銷毀的實(shí)現(xiàn)方法

    這篇文章主要介紹了Spring初始化和銷毀的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-10-10
  • Java終止線程的兩種方法

    Java終止線程的兩種方法

    本文主要介紹了Java終止線程的兩種方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • 使用maven工具解決jar包沖突或重復(fù)加載的問題

    使用maven工具解決jar包沖突或重復(fù)加載的問題

    這篇文章主要介紹了使用maven工具解決jar包沖突或重復(fù)加載的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-09-09
  • 解析SpringBoot?搭建基于?MinIO?的高性能存儲服務(wù)的問題

    解析SpringBoot?搭建基于?MinIO?的高性能存儲服務(wù)的問題

    Minio是Apache?License?v2.0下發(fā)布的對象存儲服務(wù)器,使用MinIO構(gòu)建用于機(jī)器學(xué)習(xí),分析和應(yīng)用程序數(shù)據(jù)工作負(fù)載的高性能基礎(chǔ)架構(gòu)。這篇文章主要介紹了SpringBoot?搭建基于?MinIO?的高性能存儲服務(wù),需要的朋友可以參考下
    2022-03-03
  • MyBatis全局配置文件詳解

    MyBatis全局配置文件詳解

    這篇文章主要介紹了mybatis 加載配置文件的方法,通過實(shí)例代碼給大家介紹了mybatis 加載配置文件的兩種方式,需要的朋友可以參考下
    2021-07-07
  • Springboot Maven打包跳過測試的五種方式小結(jié)

    Springboot Maven打包跳過測試的五種方式小結(jié)

    本文主要介紹了Springboot Maven打包跳過測試的五種方式小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04

最新評論