SpringBoot 2.5.5整合SpringSecurity+JWT

目錄結構 

 

 

添加依賴

<!-- SpringSecurity -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- redis客戶端 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.19</version>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ybchen</groupId>
    <artifactId>ybchen-SpringSecurity</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ybchen-SpringSecurity</name>
    <description>SpringBoot整合SpringSecurity</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- 排除tomcat容器 -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- undertow容器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

        <!-- SpringSecurity -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- redis客戶端 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.19</version>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
pom.xml

通用工具類

package com.ybchen.utils;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.MessageDigest;

/**
 * 公共工具類
 *
 * @author: chenyanbin 2022-11-13 19:17
 */
@Slf4j
public class CommonUtil {
    /**
     * 響應json數據給前端
     *
     * @param response
     * @param content
     */
    public static void sendJsonMessage(HttpServletResponse response, Object content) {
        ObjectMapper objectMapper = new ObjectMapper();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.print(objectMapper.writeValueAsString(content));
            response.flushBuffer();
        } catch (IOException e) {
            log.info("響應json數據給前端失敗:{}", e.getMessage());
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

    /**
     * md5加密
     *
     * @param data
     * @return
     */
    public static String MD5(String data) {
        try {
            java.security.MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] array = md.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString().toUpperCase();
        } catch (Exception exception) {
        }
        return null;
    }
}
CommonUtil.java
package com.ybchen.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

/**
 * jwt工具類
 *
 * @author: chenyanbin 2022-11-13 19:00
 */
@Slf4j
public class JWTUtil {
    /**
     * token過期時間,一般7天
     */
    private static final long EXPIRE = 60 * 1000 * 60 * 24 * 7;

    /**
     * 密鑰
     */
    private static final String SECRET = "https://www.cnblogs.com/chenyanbin/";

    /**
     * 令牌前綴
     */
    private static final String TOKEN_PREFIX = "ybchen";

    /**
     * subject
     */
    private static final String SUBJECT = "security_jwt";

    /**
     * 根據用戶信息,生成令牌token
     *
     * @param loginInfo 登錄信息
     * @return
     */
    public static String geneJsonWebToken(String loginInfo) {
        if (loginInfo == null || "".equalsIgnoreCase(loginInfo)) {
            throw new NullPointerException("loginInfo對象爲空");
        }
        String token = Jwts.builder()
                .setSubject(SUBJECT)
                //payLoad,負載
                .claim("loginInfo", loginInfo)
                //頒佈時間
                .setIssuedAt(new Date())
                //過期時間
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .signWith(SignatureAlgorithm.HS256, SECRET).compact();
        return TOKEN_PREFIX + token;
    }

    /**
     * 校驗令牌token
     *
     * @param token
     * @return
     */
    public static Claims checkJwt(String token) {
        try {
            final Claims body = Jwts.parser()
                    //設置簽名
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody();
            return body;
        } catch (Exception e) {
            log.error("jwt token解密失敗,錯誤信息:{}", e);
            return null;
        }
    }
}
JWTUtil.java
package com.ybchen.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Collection;
import java.util.concurrent.TimeUnit;

/**
 * redis工具類
 *
 * @author: chenyanbin 2022-11-13 19:05
 */
@Component
@Slf4j
public class RedisUtil {
    @Autowired
    RedisTemplate redisTemplate;

    /**
     * 判斷緩存中是否有對應的value
     *
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 刪除對應的value
     *
     * @param key
     */
    public void remove(final String key) {
        if (exists(key)) {
            redisTemplate.delete(key);
        }
    }

    /**
     * 寫入緩存
     *
     * @param key        緩存key
     * @param value      緩存value
     * @param expireTime 過期時間,秒
     * @return
     */
    public boolean set(final String key, Object value, Long expireTime) {
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        operations.set(key, value);
        redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
        return true;
    }

    /**
     * 原子遞增
     *
     * @param key        鍵
     * @param expireTime 過期時間,秒
     * @return
     */
    public Long incr(final String key, Long expireTime) {
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        Long increment = operations.increment(key);
        redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
        return increment;
    }

    /**
     * 原子遞增,永不過期
     *
     * @param key 鍵
     * @return
     */
    public Long incr(final String key) {
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        Long increment = operations.increment(key);
        return increment;
    }

    /**
     * 讀取緩存
     *
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    /**
     * 獲得緩存的key列表
     * <p>
     * keys token:*
     * </>
     *
     * @param pattern 字符串前綴
     * @return 對象列表
     */
    public Collection<String> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }


    /**
     * 哈希 添加(Map<Map<key,value>,value>)
     *
     * @param key     第一個Map的key
     * @param hashKey 第二個Map的key
     * @param value   第二個Map的value
     */
    public void hashSet(String key, String hashKey, Object value) {
        HashOperations<String, String, Object> hash = redisTemplate.opsForHash();
        hash.put(key, hashKey, value);
    }

    /**
     * 哈希獲取數據(Map<Map<key,value>,value>)
     *
     * @param key     第一個Map的key
     * @param hashKey 第二個Map的key
     * @return
     */
    public Object hashGet(String key, String hashKey) {
        HashOperations<String, String, Object> hash = redisTemplate.opsForHash();
        return hash.get(key, hashKey);
    }

    /**
     * 哈希刪除某個key(Map<Map<key,value>,value>)
     *
     * @param key     第一個Map的key
     * @param hashKey 第二個Map的key
     * @return
     */
    public Long hashDelete(String key, String hashKey) {
        HashOperations<String, String, Object> hash = redisTemplate.opsForHash();
        return hash.delete(key, hashKey);
    }
}
RedisUtil.java
package com.ybchen.utils;

import com.alibaba.fastjson2.JSON;

import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 統一響應工具類
 *
 * @author: chenyanbin 2022-11-13 18:48
 */
public class ReturnT<T> implements Serializable {
    /**
     * 狀態碼 0 表示成功,-1表示失敗
     */
    private Integer code;
    /**
     * 數據
     */
    private T data;
    /**
     * 描述
     */
    private String msg;

    private ReturnT() {
    }

    private static <T> ReturnT<T> build(Integer code, T data, String msg) {
        ReturnT<T> resultT = new ReturnT<>();
        resultT.code = code;
        resultT.data = data;
        resultT.msg = msg;
        return resultT;
    }

    /**
     * 成功
     *
     * @param data
     * @return
     */
    public static <T> ReturnT<T> success(T data) {
        return build(0, data, null);
    }

    /**
     * 成功
     * @param <T>
     * @return
     */
    public static <T> ReturnT<T> success() {
        return build(0, null, null);
    }

    /**
     * 失敗
     *
     * @param msg 錯誤信息
     * @return
     */
    public static <T> ReturnT<T> error(String msg) {
        return build(-1, null, msg);
    }

    /**
     * 失敗
     *
     * @param code 狀態碼
     * @param msg  錯誤信息
     * @return
     */
    public static <T> ReturnT<T> error(int code, String msg) {
        return build(code == 0 ? -1 : code, null, msg);
    }

    /**
     * 判斷接口響應是否成功
     *
     * @param data
     * @return
     */
    public static boolean isSuccess(ReturnT data) {
        return data.code == 0;
    }

    /**
     * 判斷接口響應是否失敗
     *
     * @param data
     * @return
     */
    public static boolean isFailure(ReturnT data) {
        return !isSuccess(data);
    }

    public Integer getCode() {
        return code;
    }

    public T getData() {
        return data;
    }

    public String getMsg() {
        return msg;
    }

    @Override
    public String toString() {
        Map<String, Object> resultMap = new LinkedHashMap<>(3);
        resultMap.put("code", this.code);
        resultMap.put("data", this.data);
        resultMap.put("msg", this.msg);
        return JSON.toJSONString(resultMap);
    }
}
ReturnT.java

vo類

package com.ybchen.vo;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * 登錄信息
 *
 * @author: chenyanbin 2022-11-14 16:54
 */
public class LoginInfo implements Serializable {
    /**
     * 用戶信息
     */
    private UserVo userVo;
    /**
     * 權限信息
     */
    private List<String> permissionsList = new ArrayList<>();

    private LoginInfo() {
    }

    public LoginInfo(UserVo userVo, List<String> permissionsList) {
        this.userVo = userVo;
        this.permissionsList = permissionsList;
    }

    public UserVo getUserVo() {
        return userVo;
    }

    public void setUserVo(UserVo userVo) {
        this.userVo = userVo;
    }

    public List<String> getPermissionsList() {
        return permissionsList;
    }

    public void setPermissionsList(List<String> permissionsList) {
        this.permissionsList = permissionsList;
    }

    @Override
    public String toString() {
        return "LoginInfo{" +
                "userVo=" + userVo +
                ", permissionsList=" + permissionsList +
                '}';
    }
}
LoginInfo.java
package com.ybchen.vo;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;

/**
 * 用戶對象
 *
 * @author: chenyanbin 2022-11-13 19:19
 */
@Getter
@Setter
@ToString
public class UserVo implements Serializable {
    /**
     * 用戶id
     */
    private Integer id;

    /**
     * 用戶姓名
     */
    private String userName;

    /**
     * 密碼
     */
    private String password;

    private UserVo() {
    }

    public UserVo(Integer id, String userName, String password) {
        this.id = id;
        this.userName = userName;
        this.password = password;
    }
}
UserVo.java

常量

package com.ybchen.constant;

/**
 * redis常量key
 *
 * @author: chenyanbin 2022-11-13 21:05
 */
public class RedisKeyConstant {
    /**
     * 登錄信息key
     */
    public static final String LOGIN_INFO_KEY = "user:login";
}

全局異常處理器

package com.ybchen.exception;

import com.ybchen.utils.ReturnT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;

/**
 * 全局異常攔截器
 *
 * @author: chenyanbin 2022-11-13 19:39
 */
@RestControllerAdvice
@Slf4j
public class GlobalException {

    /**
     * 請求方式有誤
     *
     * @param e
     * @return
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ReturnT httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        return ReturnT.error("請求方式有誤!");
    }

    /**
     * 請求url不存在,404問題
     *
     * @param e
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ReturnT noHandlerFoundException(NoHandlerFoundException e) {
        return ReturnT.error("請求url不存在!");
    }

    /**
     * 請求參數轉換異常
     * @param e
     * @return
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ReturnT methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        return ReturnT.error("參數轉換異常:" + e.toString());
    }

    /**
     * 請求缺失參數
     * @param e
     * @return
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ReturnT missingServletRequestParameterException(MissingServletRequestParameterException e) {
        return ReturnT.error("缺失請求參數:" + e.toString());
    }

    /**
     * SpringSecurity認證失敗處理
     * @param e
     * @return
     */
    @ExceptionHandler(BadCredentialsException.class)
    public ReturnT badCredentialsException(BadCredentialsException e){
        return ReturnT.error(401, "用戶認證失敗!");
    }

    @ExceptionHandler(AccessDeniedException.class)
    public ReturnT accessDeniedException(AccessDeniedException e){
        return ReturnT.error(403,"你的權限不足!");
    }

    /**
     * 全局異常攔截
     *
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public ReturnT exception(Exception e) {
        log.error("全局異常:{}", e);
        return ReturnT.error(e.toString());
    }
}

SpringSecurity相關

重寫UserDetailsService

  作用:重寫該方法去數據庫查找用戶信息

package com.ybchen.service;

import com.alibaba.fastjson2.annotation.JSONField;
import com.ybchen.utils.CommonUtil;
import com.ybchen.vo.UserVo;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 默認SpringSecurity是在內存中查找用戶的信息(InMemoryUserDetailsManager),
 * UserDetailsService接口定義了一個根據用戶名查詢用戶信息的方法,
 * 需要重寫UserDetailsService,改到去數據庫中查詢用戶信息
 *
 * @author: chenyanbin 2022-11-13 19:24
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    /**
     * 模擬數據庫中用戶數據
     */
    public static List<UserVo> userVoList = new ArrayList<UserVo>() {{
        // 注意數據庫中存儲的密碼,MD5加密過的,也可以加鹽
        this.add(new UserVo(1, "alex", CommonUtil.MD5("alex123")));
        this.add(new UserVo(2, "tom", CommonUtil.MD5("tom123")));
    }};

    /**
     * 獲取用戶信息
     *
     * @param userName 用戶名
     * @return 根據用戶名找用戶信息,找不到的話,返回null
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        // TODO 去數據庫中,查找用戶信息
        List<UserVo> collect = userVoList.stream().filter(obj -> obj.getUserName().equalsIgnoreCase(userName)).collect(Collectors.toList());
        // 如果用戶不存在
        if (collect.size() == 0) {
            throw new RuntimeException("用戶名不存在");
        }
        //TODO 根據用戶id查找對應的權限信息
        List<String> permissionsList = Arrays.asList("admin", "test", "hello_test", "ROLE_admin");
        //將數據封裝成UserDetails返回
        return new LoginUserSecurity(collect.get(0), permissionsList);
    }


    @Data
    public class LoginUserSecurity implements UserDetails, Serializable {
        /**
         * 用戶對象
         */
        private UserVo userVo;

        private List<String> permissionsList;

        @JSONField(serialize = false)
        private List<GrantedAuthority> authorityList;

        /**
         * 無參構造
         */
        private LoginUserSecurity() {
        }

        /**
         * 有參構造
         *
         * @param userVo          用戶對象
         * @param permissionsList 權限集合
         */
        public LoginUserSecurity(UserVo userVo, List<String> permissionsList) {
            this.userVo = userVo;
            this.permissionsList = permissionsList;
        }

        /**
         * 權限信息
         *
         * @return
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            if (authorityList != null) {
                return authorityList;
            }
            //將permissionsList中的String類型的權限信息,封裝成SimpleGrantedAuthority對象
            authorityList = permissionsList
                    .stream()
                    .map(SimpleGrantedAuthority::new)
                    .distinct()
                    .collect(Collectors.toList());
            return authorityList;
        }

        /**
         * 密碼
         *
         * @return
         */
        @Override
        public String getPassword() {
            return this.userVo.getPassword();
        }

        /**
         * 用戶名
         *
         * @return
         */
        @Override
        public String getUsername() {
            return this.userVo.getId().toString();
        }

        /**
         * 賬號是否過期
         *
         * @return
         */
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }

        /**
         * 賬號是否鎖定
         *
         * @return
         */
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }

        /**
         * 密碼是否過期
         *
         * @return
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }

        /**
         * 賬號是否啓用
         *
         * @return
         */
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
}

  注:這裏默認的用戶賬號和密碼,實際去數據庫中查詢,這邊模擬的數據密碼md5加密,所以需要重寫密碼編碼器

package com.ybchen.config;

import com.ybchen.utils.CommonUtil;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 自定義用戶密碼,重寫PasswordEncoder
 *
 * @author: chenyanbin 2022-11-13 19:53
 */
public class Md5PasswordEncoder implements PasswordEncoder {

    /**
     * 加密
     *
     * @param rawPassword 原始密碼
     * @return
     */
    @Override
    public String encode(CharSequence rawPassword) {
        return CommonUtil.MD5(rawPassword.toString());
    }

    /**
     * 匹配密碼
     *
     * @param rawPassword     原始密碼
     * @param encodedPassword 存儲的密碼
     * @return
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return CommonUtil.MD5(rawPassword.toString()).equalsIgnoreCase(encodedPassword);
    }
}

  將自定義密碼編碼器注入spring容器

package com.ybchen.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * app配置類
 * @author: chenyanbin 2022-11-13 20:12
 */
@Configuration
public class AppConfig {

    /**
     * 自定義SpringSecurity MD5編碼器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new Md5PasswordEncoder();
    }
}

jwt過濾器

package com.ybchen.filter;

import com.alibaba.fastjson2.JSON;
import com.ybchen.constant.RedisKeyConstant;
import com.ybchen.utils.CommonUtil;
import com.ybchen.utils.JWTUtil;
import com.ybchen.utils.RedisUtil;
import com.ybchen.utils.ReturnT;
import com.ybchen.vo.LoginInfo;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Collectors;

/**
 * jwt過濾器
 *
 * @author: chenyanbin 2022-11-13 21:22
 */
@Component
public class JwtFilter extends OncePerRequestFilter {
    @Autowired
    RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //獲取token
        String token = request.getHeader("token");
        if (token == null || "".equalsIgnoreCase(token)) {
            token = request.getParameter("token");
        }
        if (token == null || "".equalsIgnoreCase(token)) {
            //放行
            filterChain.doFilter(request, response);
        } else {
            //解析token
            Claims claims = JWTUtil.checkJwt(token);
            if (claims == null) {
                CommonUtil.sendJsonMessage(response, ReturnT.error("token非法"));
                return;
            }
            //從redis中獲取用戶信息
            Integer id = Integer.parseInt(claims.get("loginInfo").toString());
            Object objValue = redisUtil.hashGet(RedisKeyConstant.LOGIN_INFO_KEY, id + "");
            if (objValue == null) {
                CommonUtil.sendJsonMessage(response, ReturnT.error("token過期或已註銷登錄"));
                return;
            }
            LoginInfo loginInfo = JSON.parseObject(JSON.toJSONString(objValue), LoginInfo.class);
            //存入SecurityContextHolder
            // 獲取權限信息封裝到Authentication
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    loginInfo,
                    null,
                    loginInfo.getPermissionsList().stream()
                            .map(SimpleGrantedAuthority::new)
                            .distinct()
                            .collect(Collectors.toList())
            );
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            //放行
            filterChain.doFilter(request, response);
        }
    }
}

跨域處理

package com.ybchen.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 跨域配置類
 * @author: chenyanbin 2022-11-14 20:47
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //設置允許跨域的路徑
        registry.addMapping("/**")
                //設置允許跨域請求的域名
                .allowedOriginPatterns("*")
                //是否允許cookie
                .allowCredentials(true)
                //設置允許的請求方式
                .allowedMethods("GET","POST","DELETE","PUT")
                //設置允許的Header屬性
                .allowedHeaders("*")
                //跨域允許時間,秒
                .maxAge(3600);
    }
}

redis配置類

package com.ybchen.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置類,處理中文亂碼
 * @author: chenyanbin 2022-11-13 18:47
 */
@Configuration
public class RedisTemplateConfiguration {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        //配置序列化規則
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        serializer.setObjectMapper(objectMapper);
        //設置key-value序列化規則
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        //設置hash-value序列化規則
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        return redisTemplate;
    }
}

SpringSecurity配置類

package com.ybchen.config;

import com.ybchen.filter.JwtFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * SpringSecurity配置類
 *
 * @author: chenyanbin 2022-11-13 20:36
 */
@Configuration
//開啓SpringSecurity的prePostEnabled配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    JwtFilter jwtFilter;
//    @Autowired
//    AuthenticationEntryPoint authenticationEntryPoint;
//    @Autowired
//    AccessDeniedHandler accessDeniedHandler;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //關閉csrf
                .csrf().disable()
                //不通過Session獲取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //對於登錄接口,允許匿名訪問
                .antMatchers("/api/v1/user/login").anonymous()
                //除了匿名訪問的所有請求,全部需要鑑權認證
                .anyRequest().authenticated();
        //添加過濾器
        http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        //配置異常處理器,也可以用全局異常攔截器,攔截:AccessDeniedException,BadCredentialsException
//        http.exceptionHandling()
//                //認證失敗處理器
//                .authenticationEntryPoint(authenticationEntryPoint)
//                //授權失敗處理器
//                .accessDeniedHandler(accessDeniedHandler);
        //允許跨域
        http.cors();
    }
}

  2個異常處理器,當然也可以使用SpringBoot全局異常攔截處理,也可以寫到SpringSecurity配置類中

//package com.ybchen.config;
//
//import com.ybchen.utils.CommonUtil;
//import com.ybchen.utils.ReturnT;
//import lombok.extern.slf4j.Slf4j;
//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;
//
///**
// * 認證失敗處理器
// *
// * @author: chenyanbin 2022-11-14 13:06
// */
//@Component
//@Slf4j
//public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
//
//    @Override
//    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
//        log.error("認證失敗:{}", e);
//        CommonUtil.sendJsonMessage(response, ReturnT.error(401, "用戶認證失敗"));
//    }
//}
//package com.ybchen.config;
//
//import com.ybchen.utils.CommonUtil;
//import com.ybchen.utils.ReturnT;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.security.access.AccessDeniedException;
//import org.springframework.security.web.access.AccessDeniedHandler;
//import org.springframework.stereotype.Component;
//
//import javax.servlet.ServletException;
//import javax.servlet.http.HttpServletRequest;
//import javax.servlet.http.HttpServletResponse;
//import java.io.IOException;
//
///**
// * 授權失敗處理器
// * @author: chenyanbin 2022-11-14 13:11
// */
//@Component
//@Slf4j
//public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
//
//    @Override
//    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//        log.error("授權失敗:{}",e);
//        CommonUtil.sendJsonMessage(response, ReturnT.error(403,"你的權限不足!"));
//    }
//}

控制層

package com.ybchen.controller;

import com.ybchen.constant.RedisKeyConstant;
import com.ybchen.service.UserDetailServiceImpl;
import com.ybchen.utils.JWTUtil;
import com.ybchen.utils.RedisUtil;
import com.ybchen.utils.ReturnT;
import com.ybchen.vo.LoginInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;

/**
 * 用戶api
 *
 * @author: chenyanbin 2022-11-13 20:27
 */
@RestController
@RequestMapping("/api/v1/user")
@Slf4j
public class UserController {
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    RedisUtil redisUtil;

    /**
     * 用戶登錄
     *
     * @param userName 用戶名
     * @param password 密碼
     * @return
     */
    @GetMapping("login")
    public ReturnT login(
            @RequestParam(value = "userName", required = true) String userName,
            @RequestParam(value = "password", required = true) String password
    ) {
        //AuthenticationManager authenticate進行用戶認證-----》其實是調用UserDetailsService.loadUserByUsername方法
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, password);
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果認證沒通過,給出對應的提示
        if (Objects.isNull(authenticate)) {
            return ReturnT.error("賬號/密碼錯誤");
        }
        //用戶id
        UserDetailServiceImpl.LoginUserSecurity loginUser = (UserDetailServiceImpl.LoginUserSecurity) authenticate.getPrincipal();
        Integer userId = loginUser.getUserVo().getId();
        String token = JWTUtil.geneJsonWebToken(userId.toString());
        //token寫入redis Hash
        redisUtil.hashSet(RedisKeyConstant.LOGIN_INFO_KEY, userId + "", new LoginInfo(loginUser.getUserVo(), loginUser.getPermissionsList()));
        log.info("token= \n {}", token);
        return ReturnT.success(token);
    }

    /**
     * 註銷登錄
     *
     * @return
     */
    @GetMapping("logout")
    public ReturnT logout() {
        //獲取SecurityContextHolder中的用戶id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginInfo loginInfo = (LoginInfo) authentication.getPrincipal();
        Integer id = loginInfo.getUserVo().getId();
        //刪除redis的key
        redisUtil.hashDelete(RedisKeyConstant.LOGIN_INFO_KEY, id + "");
        return ReturnT.success("註銷成功");
    }
}
package com.ybchen.controller;

import com.ybchen.utils.ReturnT;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: chenyanbin 2022-11-13 18:03
 */
@RestController
public class HelloController {
    /**
     * -@PreAuthorize()
     * ---hasAuthority:底層調用的是UserDetailsService.getAuthorities(),也就是我們重寫的方法,判斷入參是否在Set集合中,true放行,false攔截
     * ---hasAnyAuthority:可以傳入多個權限,只有用戶有其中任意一個權限都可以訪問對應資源
     * ---hasRole:要求有對應的角色纔可以訪問,但是他內部會把我們傳入的參數前面拼接:ROLE_ 後在比較。所以我們定義用戶權限也要加這個前綴:ROLE_
     * ---hasAnyRole:可以傳入多個角色,有任意一個角色就可以訪問資源
     *
     * @return
     */
    @GetMapping("hello")
    //加權限
//    @PreAuthorize("hasAuthority('admin')")
//    @PreAuthorize("hasAnyAuthority('admin','test')")
//    @PreAuthorize("hasRole('admin')")
    @PreAuthorize("hasAnyRole('admin','test')")
    public ReturnT<String> hello() {
        return ReturnT.success("博客地址:https://www.cnblogs.com/chenyanbin/");
    }
}

項目源碼

鏈接: https://pan.baidu.com/s/1h6NFpZ7HC9DY8s820hctTQ?pwd=h8um 提取碼: h8um 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章