Spring Security+JWT實現(xiàn)認證與授權的實現(xiàn)
認證:驗證當前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認具體是哪個用戶
授權:經(jīng)過認證后判斷當前用戶是否有權限進行某個操作
一、登錄校驗流程

1、Spring Security 完整流程
SpringSecurity的原理其實就是一個過濾器鏈,內(nèi)部包含了提供各種功能的過濾器。部分核心過濾器如下圖:

UsernamePasswordAuthenticationFilter:負責處理在登錄頁填寫了用戶名密碼后的登陸請求。
ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException(訪問出錯)和AuthenticationExcption(認證出錯)。
FilterSecurityInterceptor:負責權限校驗的過濾器。
2、Spring Security的默認登陸驗證流程。




Authentication接口:它的實現(xiàn)類,表示當前訪問系統(tǒng)的用戶,封裝了用戶相關信息。
AuthenticationManager接口:定義了認證Authentication的方法。
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的方法。
UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。
3、 整合JWT大致流程
登錄
①自定義登錄接口
調(diào)用ProviderManager的方法進行認證 如果認證通過生成JWT。
把用戶信息存入redis中
②自定義UserDetailsService
在這個實現(xiàn)類中去查詢數(shù)據(jù)庫
校驗
①定義Jwt認證過濾器
獲取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、實體類等
*
* @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、實體類等
*
* @param key 緩存的鍵值
* @param value 緩存的值
* @param timeout 時間
* @param timeUnit 時間顆粒度
* @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 緩存鍵值對應的數(shù)據(jù)
*/
public Object getCacheObject(Object key) {
ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 刪除單個對象
*
* @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 緩存鍵值對應的數(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);
}
}前端響應類
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
/**
* 狀態(tài)碼
*/
private Integer code;
/**
* 提示信息,如果有錯誤時,前端可以獲取該字段進行提示
*/
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 一個小時
//設置秘鑰明文
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());// 設置過期時間
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的數(shù)據(jù)(json格式)
* @param ttlMillis token超時時間
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設置過期時間
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ā)時間
.signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個參數(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);// 設置過期時間
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ù)據(jù)庫連接信息
定義mapper等一系列接口。xml等。用mybatis-plus方便一點,注意Mapper繼承BaseMapper<實體類>,實體類中需要加@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)建一個類實現(xiàn)UserDetailsService接口,重寫其中的方法。從數(shù)據(jù)庫中查詢用戶信息,進行校驗。(如果沒有重寫的話,就是上面說的spring security默認的使用UserDetailsService接口下面的InMemoryUserDetailsManager實現(xiàn)類中的方法,是在內(nèi)存中查找。這個是需要根據(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("用戶名或者密碼錯誤");
}
// 查詢權限
List<String> list = menuMapper.selectPermsByUserId(user.getId());
//把數(shù)據(jù)封裝成UserDetails返回
return new LoginUser(user,list);
}
}因為UserDetailsService方法的返回值是UserDetails類型,所以需要定義一個類,實現(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類型的權限信息封裝成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對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。
在接口中我們通過AuthenticationManager的authenticate方法來進行用戶認證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
認證成功的話要生成一個jwt,放入響應中返回。并且為了讓用戶下回請求時能通過jwt識別出具體
的是哪個用戶,我們需要把用戶信息存入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 //關閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous()// .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權認證 .anyRequest().authenticated(); //添加過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置異常處理器 http.exceptionHandling() //配置認證失敗處理器 .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
//關閉csrf
.csrf().disable()
//不通過Session獲取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 對于登錄接口 允許匿名訪問
.antMatchers("/user/login").anonymous()
// .antMatchers("/testCors").hasAuthority("system:dept:list222")
// 除上面外的所有請求全部需要鑒權認證
.anyRequest().authenticated();
//添加過濾器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//配置異常處理器
http.exceptionHandling()
//配置認證失敗處理器
.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進行用戶認證
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果認證沒通過,給出對應的提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登錄失敗");
}
//如果認證通過了,使用userid生成一個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,"注銷成功");
}
}認證過濾器
我們需要自定義一個過濾器,這個過濾器會去獲取請求頭中的token,對token進行解析取出其中的userid。(把這個放到最前面,放到UsernamePassword的那個前面)這樣做就是為了除了登錄的時候去查詢數(shù)據(jù)庫外,其他時候都用JWT配合Redis進行認證。
使用userid去redis中獲取對應的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 獲取權限信息封裝到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}退出登陸
我們只需要定義一個登陸接口,然后獲取SecurityContextHolder中的認證信息,刪除redis中對應的數(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,"注銷成功");
}授權基本流程
在SpringSecurity中,會使用默認的FilterSecurityInterceptor來進行權限校驗。在FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權限信息。當前用戶是否擁有訪問當前資源所需的權限。
所以我們在項目中只需要把當前登錄用戶的權限信息也存入Authentication。
然后設置我們的資源所需要的權限即可。
限制訪問資源所需權限
SpringSecurity為我們提供了基于注解的權限控制方案,這也是我們項目中主要采用的方式。我們可以使用注解去指定訪問對應的資源所需的權限。
但是要使用它我們需要先開啟相關配置。

然后就可以使用對應的注解。@PreAuthorize
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "hello";
}
}封裝權限信息
我們前面在寫UserDetailsServiceImpl的時候說過,在查詢出用戶后還要獲取對應的權限信息,封裝到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("用戶名或密碼錯誤");
}
//TODO 根據(jù)用戶查詢權限信息 添加到LoginUser中
List<String> list = new ArrayList<>(Arrays.asList("test"));
return new LoginUser(user,list);
}
}RBAC權限模型
RBAC權限模型(Role-Based Access Control)即:基于角色的權限控制。這是目前最常被開發(fā)者使用也是相對易用、通用權限模型。

參考表:
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 '權限標識', `icon` varchar(100) DEFAULT '#' COMMENT '菜單圖標', `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 '角色權限字符串', `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 '角色ID', `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 '手機號', `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)建時間', `update_by` bigint(20) DEFAULT NULL COMMENT '更新人', `update_time` datetime DEFAULT NULL COMMENT '更新時間', `del_flag` int(11) DEFAULT '0' COMMENT '刪除標志(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)實體類
*
* @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;
/**
* 權限標識
*/
private String perms;
/**
* 菜單圖標
*/
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
自定義失敗處理
我們還希望在認證失敗或者是授權失敗的情況下也能和我們的接口一樣返回相同結(jié)構的json,這樣可以讓前端能對響應進行統(tǒng)一的處理。要實現(xiàn)這個功能我們需要知道SpringSecurity的異常處理機制。
在SpringSecurity中,如果我們在認證或者授權的過程中出現(xiàn)了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認證失敗還是授權失敗出現(xiàn)的異常。
如果是認證過程中出現(xiàn)的異常會被封裝成AuthenticationException然后調(diào)用AuthenticationEntryPoint對象的方法去進行異常處理。
如果是授權過程中出現(xiàn)的異常會被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對象的方法去進行異常處理。
所以如果我們需要自定義異常處理,我們只需要自定義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(), "權限不足");
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(), "認證失敗請重新登錄");
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

到此這篇關于Spring Security+JWT實現(xiàn)認證與授權的實現(xiàn)的文章就介紹到這了,更多相關Spring Security JWT認證與授權內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- SpringBoot+SpringSecurity+JWT實現(xiàn)系統(tǒng)認證與授權示例
- mall整合SpringSecurity及JWT認證授權實戰(zhàn)下
- mall整合SpringSecurity及JWT實現(xiàn)認證授權實戰(zhàn)
- Spring?Security使用數(shù)據(jù)庫登錄認證授權
- Java Spring Security認證與授權及注銷和權限控制篇綜合解析
- SpringSecurity數(shù)據(jù)庫進行認證和授權的使用
- SpringBoot+SpringSecurity實現(xiàn)基于真實數(shù)據(jù)的授權認證
- Spring Security OAuth2認證授權示例詳解
- Spring Security實現(xiàn)身份認證和授權的示例代碼
相關文章
SpringBoot中@EnableAsync和@Async注解的使用小結(jié)
在SpringBoot中,可以通過@EnableAsync注解來啟動異步方法調(diào)用的支持,通過@Async注解來標識異步方法,讓方法能夠在異步線程中執(zhí)行,本文就來介紹一下,感興趣的可以了解一下2023-11-11
spring?retry方法調(diào)用失敗重試機制示例解析
這篇文章主要為大家介紹了spring?retry方法調(diào)用失敗重試機制的示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2022-03-03

