目錄結構
添加依賴
<!-- 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>
通用工具類
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; } }
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; } } }
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); } }
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); } }
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 + '}'; } }
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; } }
常量
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