Spring Security+JWT實(shí)現(xiàn)認(rèn)證與授權(quán)的實(shí)現(xiàn)
認(rèn)證:驗(yàn)證當(dāng)前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認(rèn)具體是哪個(gè)用戶
授權(quán):經(jīng)過認(rèn)證后判斷當(dāng)前用戶是否有權(quán)限進(jìn)行某個(gè)操作
一、登錄校驗(yàn)流程
1、Spring Security 完整流程
SpringSecurity的原理其實(shí)就是一個(gè)過濾器鏈,內(nèi)部包含了提供各種功能的過濾器。部分核心過濾器如下圖:
UsernamePasswordAuthenticationFilter:負(fù)責(zé)處理在登錄頁填寫了用戶名密碼后的登陸請求。
ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException(訪問出錯(cuò))和AuthenticationExcption(認(rèn)證出錯(cuò))。
FilterSecurityInterceptor:負(fù)責(zé)權(quán)限校驗(yàn)的過濾器。
2、Spring Security的默認(rèn)登陸驗(yàn)證流程。
Authentication接口:它的實(shí)現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
AuthenticationManager接口:定義了認(rèn)證Authentication的方法。
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的方法。
UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。
3、 整合JWT大致流程
登錄
①自定義登錄接口
調(diào)用ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過生成JWT。
把用戶信息存入redis中
②自定義UserDetailsService
在這個(gè)實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫
校驗(yàn)
①定義Jwt認(rèn)證過濾器
獲取token
解析token獲取其中的userid
從redis中獲取用戶信息
存入SecurityContextHolder
Redis使用Fastjson序列化
<!-- spring data redis 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- commons-pool2 對象池依賴 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- JSON工具 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency>
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } private final Class<T> clazz; public FastJson2JsonRedisSerializer(Class<T> clazz) { super(); this.clazz = clazz; } /** * 序列化 */ @Override public byte[] serialize(T t) throws SerializationException { if (null == t) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } /** * 反序列化 */ @Override public T deserialize(byte[] bytes) throws SerializationException { if (null == bytes || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return (T) JSON.parseObject(str, clazz); } }
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisCacheAutoConfiguration { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); FastJson2JsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJson2JsonRedisSerializer<>(Object.class); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用fastJson template.setValueSerializer(fastJsonRedisSerializer); // hash的value序列化方式采用fastJson template.setHashValueSerializer(fastJsonRedisSerializer); template.afterPropertiesSet(); return template; } }
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.*; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.TimeUnit; /** * spring redis 工具類 **/ @Component public class RedisUtil { @Autowired private RedisTemplate<Object, Object> redisTemplate; /** * 緩存基本的對象,Integer、String、實(shí)體類等 * * @param key 緩存的鍵值 * @param value 緩存的值 * @return 緩存的對象 */ public ValueOperations<Object, Object> setCacheObject(Object key, Object value) { ValueOperations<Object, Object> operation = redisTemplate.opsForValue(); operation.set(key, value); return operation; } /** * 緩存基本的對象,Integer、String、實(shí)體類等 * * @param key 緩存的鍵值 * @param value 緩存的值 * @param timeout 時(shí)間 * @param timeUnit 時(shí)間顆粒度 * @return 緩存的對象 */ public ValueOperations<Object, Object> setCacheObject(Object key, Object value, Integer timeout, TimeUnit timeUnit) { ValueOperations<Object, Object> operation = redisTemplate.opsForValue(); operation.set(key, value, timeout, timeUnit); return operation; } /** * 獲得緩存的基本對象。 * * @param key 緩存鍵值 * @return 緩存鍵值對應(yīng)的數(shù)據(jù) */ public Object getCacheObject(Object key) { ValueOperations<Object, Object> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 刪除單個(gè)對象 * * @param key */ public void deleteObject(Object key) { redisTemplate.delete(key); } /** * 刪除集合對象 * * @param collection */ public void deleteObject(Collection collection) { redisTemplate.delete(collection); } public Long getExpire(String key) { return redisTemplate.getExpire(key); } public void expire(String key, int expire, TimeUnit timeUnit) { redisTemplate.expire(key, expire, timeUnit); } /** * 緩存List數(shù)據(jù) * * @param key 緩存的鍵值 * @param dataList 待緩存的List數(shù)據(jù) * @return 緩存的對象 */ public ListOperations<Object, Object> setCacheList(Object key, List<Object> dataList) { ListOperations listOperation = redisTemplate.opsForList(); if (null != dataList) { int size = dataList.size(); for (Object o : dataList) { listOperation.leftPush(key, o); } } return listOperation; } /** * 獲得緩存的list對象 * * @param key 緩存的鍵值 * @return 緩存鍵值對應(yīng)的數(shù)據(jù) */ public List<Object> getCacheList(String key) { List<Object> dataList = new ArrayList<>(); ListOperations<Object, Object> listOperation = redisTemplate.opsForList(); Long size = listOperation.size(key); if (null != size) { for (int i = 0; i < size; i++) { dataList.add(listOperation.index(key, i)); } } return dataList; } /** * 緩存Set * * @param key 緩存鍵值 * @param dataSet 緩存的數(shù)據(jù) * @return 緩存數(shù)據(jù)的對象 */ public BoundSetOperations<Object, Object> setCacheSet(String key, Set<Object> dataSet) { BoundSetOperations<Object, Object> setOperation = redisTemplate.boundSetOps(key); for (Object o : dataSet) { setOperation.add(o); } return setOperation; } /** * 獲得緩存的set * * @param key * @return */ public Set<Object> getCacheSet(Object key) { Set<Object> dataSet = new HashSet<>(); BoundSetOperations<Object, Object> operation = redisTemplate.boundSetOps(key); dataSet = operation.members(); return dataSet; } /** * 緩存Map * * @param key * @param dataMap * @return */ public HashOperations<Object, Object, Object> setCacheMap(Object key, Map<Object, Object> dataMap) { HashOperations hashOperations = redisTemplate.opsForHash(); if (null != dataMap) { for (Map.Entry<Object, Object> entry : dataMap.entrySet()) { hashOperations.put(key, entry.getKey(), entry.getValue()); } } return hashOperations; } /** * 獲得緩存的Map * * @param key * @return */ public Map<Object, Object> getCacheMap(Object key) { Map<Object, Object> map = redisTemplate.opsForHash().entries(key); return map; } /** * 獲得緩存的基本對象列表 * * @param pattern 字符串前綴 * @return 對象列表 */ public Collection<Object> keys(String pattern) { return redisTemplate.keys(pattern); } }
前端響應(yīng)類
@JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult<T> { /** * 狀態(tài)碼 */ private Integer code; /** * 提示信息,如果有錯(cuò)誤時(shí),前端可以獲取該字段進(jìn)行提示 */ private String msg; /** * 查詢到的結(jié)果數(shù)據(jù), */ private T data; public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public ResponseResult(Integer code, T data) { this.code = code; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } public ResponseResult(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; }
JWT工具類
public class JwtUtil { //有效期為 public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一個(gè)小時(shí) //設(shè)置秘鑰明文 public static final String JWT_KEY = "zhangao"; public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } /** * 生成jtw * @param subject token中要存放的數(shù)據(jù)(json格式) * @return */ public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 設(shè)置過期時(shí)間 return builder.compact(); } /** * 生成jtw * @param subject token中要存放的數(shù)據(jù)(json格式) * @param ttlMillis token超時(shí)時(shí)間 * @return */ public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設(shè)置過期時(shí)間 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主題 可以是JSON數(shù)據(jù) .setIssuer("sg") // 簽發(fā)者 .setIssuedAt(now) // 簽發(fā)時(shí)間 .signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個(gè)參數(shù)為秘鑰 .setExpiration(expDate); } /** * 創(chuàng)建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 設(shè)置過期時(shí)間 return builder.compact(); } public static void main(String[] args) throws Exception { // String jwt = createJWT("2123"); Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0"); String subject = claims.getSubject(); System.out.println(subject); // System.out.println(claims); } /** * 生成加密后的秘鑰 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
創(chuàng)建數(shù)據(jù)庫表信息和實(shí)體,配置數(shù)據(jù)庫連接信息
定義mapper等一系列接口。xml等。用mybatis-plus方便一點(diǎn),注意Mapper繼承BaseMapper<實(shí)體類>,實(shí)體類中需要加@TableName(value = "表名") ,id字段上加 @TableId
在application.yml中配置mapperXML文件的位置
引入依賴
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
重寫UserDetailsService的方法
創(chuàng)建一個(gè)類實(shí)現(xiàn)UserDetailsService接口,重寫其中的方法。從數(shù)據(jù)庫中查詢用戶信息,進(jìn)行校驗(yàn)。(如果沒有重寫的話,就是上面說的spring security默認(rèn)的使用UserDetailsService接口下面的InMemoryUserDetailsManager實(shí)現(xiàn)類中的方法,是在內(nèi)存中查找。這個(gè)是需要根據(jù)我們具體的系統(tǒng)來重寫的。)
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查詢用戶信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); //如果沒有查詢到用戶就拋出異常 if(Objects.isNull(user)){ throw new RuntimeException("用戶名或者密碼錯(cuò)誤"); } // 查詢權(quán)限 List<String> list = menuMapper.selectPermsByUserId(user.getId()); //把數(shù)據(jù)封裝成UserDetails返回 return new LoginUser(user,list); } }
因?yàn)閁serDetailsService方法的返回值是UserDetails類型,所以需要定義一個(gè)類,實(shí)現(xiàn)該接口,把用戶信息封裝在其中。
@Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; private List<String> permissions; public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if(authorities!=null){ return authorities; } //把permissions中String類型的權(quán)限信息封裝成SimpleGrantedAuthority對象 // authorities = new ArrayList<>(); // for (String permission : permissions) { // SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission); // authorities.add(authority); // } authorities = permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
重寫登錄接口
接下我們需要自定義登陸接口,然后讓SpringSecurity對這個(gè)接口放行,讓用戶訪問這個(gè)接口的時(shí)候不用登錄也能訪問。
在接口中我們通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
認(rèn)證成功的話要生成一個(gè)jwt,放入響應(yīng)中返回。并且為了讓用戶下回請求時(shí)能通過jwt識別出具體
的是哪個(gè)用戶,我們需要把用戶信息存入redis,可以把用戶id作為key。
@RestController public class LoginController { @Autowired private LoginServcie loginServcie; @PostMapping("/user/login") public ResponseResult login(@RequestBody User user){ //登錄 return loginServcie.login(user); } @RequestMapping("/user/logout") public ResponseResult logout(){ return loginServcie.logout(); } }
@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous()// .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證 .anyRequest().authenticated(); //添加過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置異常處理器 http.exceptionHandling() //配置認(rèn)證失敗處理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允許跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous() // .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證 .anyRequest().authenticated(); //添加過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置異常處理器 http.exceptionHandling() //配置認(rèn)證失敗處理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允許跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { //AuthenticationManager authenticate進(jìn)行用戶認(rèn)證 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //如果認(rèn)證沒通過,給出對應(yīng)的提示 if(Objects.isNull(authenticate)){ throw new RuntimeException("登錄失敗"); } //如果認(rèn)證通過了,使用userid生成一個(gè)jwt jwt存入ResponseResult返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userid = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userid); Map<String,String> map = new HashMap<>(); map.put("token",jwt); //把完整的用戶信息存入redis userid作為key redisCache.setCacheObject("login:"+userid,loginUser); return new ResponseResult(200,"登錄成功",map); } @Override public ResponseResult logout() { //獲取SecurityContextHolder中的用戶id UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userid = loginUser.getUser().getId(); //刪除redis中的值 redisCache.deleteObject("login:"+userid); return new ResponseResult(200,"注銷成功"); } }
認(rèn)證過濾器
我們需要自定義一個(gè)過濾器,這個(gè)過濾器會去獲取請求頭中的token,對token進(jìn)行解析取出其中的userid。(把這個(gè)放到最前面,放到UsernamePassword的那個(gè)前面)這樣做就是為了除了登錄的時(shí)候去查詢數(shù)據(jù)庫外,其他時(shí)候都用JWT配合Redis進(jìn)行認(rèn)證。
使用userid去redis中獲取對應(yīng)的LoginUser對象。
然后封裝Authentication對象存入SecurityContextHolder
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //獲取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { //放行 filterChain.doFilter(request, response); return; } //解析token String userid; try { Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //從redis中獲取用戶信息 String redisKey = "login:" + userid; LoginUser loginUser = redisCache.getCacheObject(redisKey); if(Objects.isNull(loginUser)){ throw new RuntimeException("用戶未登錄"); } //存入SecurityContextHolder //TODO 獲取權(quán)限信息封裝到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //放行 filterChain.doFilter(request, response); } }
退出登陸
我們只需要定義一個(gè)登陸接口,然后獲取SecurityContextHolder中的認(rèn)證信息,刪除redis中對應(yīng)的數(shù)據(jù)即可。
@Override public ResponseResult logout() { //獲取SecurityContextHolder中的用戶id UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userid = loginUser.getUser().getId(); //刪除redis中的值 redisCache.deleteObject("login:"+userid); return new ResponseResult(200,"注銷成功"); }
授權(quán)基本流程
在SpringSecurity中,會使用默認(rèn)的FilterSecurityInterceptor來進(jìn)行權(quán)限校驗(yàn)。在FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權(quán)限信息。當(dāng)前用戶是否擁有訪問當(dāng)前資源所需的權(quán)限。
所以我們在項(xiàng)目中只需要把當(dāng)前登錄用戶的權(quán)限信息也存入Authentication。
然后設(shè)置我們的資源所需要的權(quán)限即可。
限制訪問資源所需權(quán)限
SpringSecurity為我們提供了基于注解的權(quán)限控制方案,這也是我們項(xiàng)目中主要采用的方式。我們可以使用注解去指定訪問對應(yīng)的資源所需的權(quán)限。
但是要使用它我們需要先開啟相關(guān)配置。
然后就可以使用對應(yīng)的注解。@PreAuthorize
@RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("hasAuthority('test')") public String hello(){ return "hello"; } }
封裝權(quán)限信息
我們前面在寫UserDetailsServiceImpl的時(shí)候說過,在查詢出用戶后還要獲取對應(yīng)的權(quán)限信息,封裝到UserDetails中返回。
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); if(Objects.isNull(user)){ throw new RuntimeException("用戶名或密碼錯(cuò)誤"); } //TODO 根據(jù)用戶查詢權(quán)限信息 添加到LoginUser中 List<String> list = new ArrayList<>(Arrays.asList("test")); return new LoginUser(user,list); } }
RBAC權(quán)限模型
RBAC權(quán)限模型(Role-Based Access Control)即:基于角色的權(quán)限控制。這是目前最常被開發(fā)者使用也是相對易用、通用權(quán)限模型。
參考表:
CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */; USE `sg_security`; /*Table structure for table `sys_menu` */ DROP TABLE IF EXISTS `sys_menu`; CREATE TABLE `sys_menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜單名', `path` varchar(200) DEFAULT NULL COMMENT '路由地址', `component` varchar(255) DEFAULT NULL COMMENT '組件路徑', `visible` char(1) DEFAULT '0' COMMENT '菜單狀態(tài)(0顯示 1隱藏)', `status` char(1) DEFAULT '0' COMMENT '菜單狀態(tài)(0正常 1停用)', `perms` varchar(100) DEFAULT NULL COMMENT '權(quán)限標(biāo)識', `icon` varchar(100) DEFAULT '#' COMMENT '菜單圖標(biāo)', `create_by` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `del_flag` int(11) DEFAULT '0' COMMENT '是否刪除(0未刪除 1已刪除)', `remark` varchar(500) DEFAULT NULL COMMENT '備注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜單表'; /*Table structure for table `sys_role` */ DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL, `role_key` varchar(100) DEFAULT NULL COMMENT '角色權(quán)限字符串', `status` char(1) DEFAULT '0' COMMENT '角色狀態(tài)(0正常 1停用)', `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag', `create_by` bigint(200) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` bigint(200) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `remark` varchar(500) DEFAULT NULL COMMENT '備注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; /*Table structure for table `sys_role_menu` */ DROP TABLE IF EXISTS `sys_role_menu`; CREATE TABLE `sys_role_menu` ( `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色I(xiàn)D', `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜單id', PRIMARY KEY (`role_id`,`menu_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; /*Table structure for table `sys_user` */ DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用戶名', `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵稱', `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密碼', `status` char(1) DEFAULT '0' COMMENT '賬號狀態(tài)(0正常 1停用)', `email` varchar(64) DEFAULT NULL COMMENT '郵箱', `phonenumber` varchar(32) DEFAULT NULL COMMENT '手機(jī)號', `sex` char(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)', `avatar` varchar(128) DEFAULT NULL COMMENT '頭像', `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用戶類型(0管理員,1普通用戶)', `create_by` bigint(20) DEFAULT NULL COMMENT '創(chuàng)建人的用戶id', `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間', `update_by` bigint(20) DEFAULT NULL COMMENT '更新人', `update_time` datetime DEFAULT NULL COMMENT '更新時(shí)間', `del_flag` int(11) DEFAULT '0' COMMENT '刪除標(biāo)志(0代表未刪除,1代表已刪除)', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表'; /*Table structure for table `sys_user_role` */ DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用戶id', `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id', PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
查詢條件
SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = 2 AND r.`status` = 0 AND m.`status` = 0
/** * 菜單表(Menu)實(shí)體類 * * @author makejava * @since 2021-11-24 15:30:08 */ @TableName(value="sys_menu") @Data @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Menu implements Serializable { private static final long serialVersionUID = -54979041104113736L; @TableId private Long id; /** * 菜單名 */ private String menuName; /** * 路由地址 */ private String path; /** * 組件路徑 */ private String component; /** * 菜單狀態(tài)(0顯示 1隱藏) */ private String visible; /** * 菜單狀態(tài)(0正常 1停用) */ private String status; /** * 權(quán)限標(biāo)識 */ private String perms; /** * 菜單圖標(biāo) */ private String icon; private Long createBy; private Date createTime; private Long updateBy; private Date updateTime; /** * 是否刪除(0未刪除 1已刪除) */ private Integer delFlag; /** * 備注 */ private String remark; }
public interface MenuMapper extends BaseMapper<Menu> { List<String> selectPermsByUserId(Long id); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sangeng.mapper.MenuMapper"> <select id="selectPermsByUserId" resultType="java.lang.String"> SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = #{userid} AND r.`status` = 0 AND m.`status` = 0 </select> </mapper>
mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml
自定義失敗處理
我們還希望在認(rèn)證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對響應(yīng)進(jìn)行統(tǒng)一的處理。要實(shí)現(xiàn)這個(gè)功能我們需要知道SpringSecurity的異常處理機(jī)制。
在SpringSecurity中,如果我們在認(rèn)證或者授權(quán)的過程中出現(xiàn)了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。
如果是認(rèn)證過程中出現(xiàn)的異常會被封裝成AuthenticationException然后調(diào)用AuthenticationEntryPoint對象的方法去進(jìn)行異常處理。
如果是授權(quán)過程中出現(xiàn)的異常會被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對象的方法去進(jìn)行異常處理。
所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置給SpringSecurity即可。
@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "權(quán)限不足"); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "認(rèn)證失敗請重新登錄"); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }
public class WebUtils { /** * 將字符串渲染到客戶端 * * @param response 渲染對象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
配置給SpringSecurity
到此這篇關(guān)于Spring Security+JWT實(shí)現(xiàn)認(rèn)證與授權(quán)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Spring Security JWT認(rèn)證與授權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot+SpringSecurity+JWT實(shí)現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
- mall整合SpringSecurity及JWT認(rèn)證授權(quán)實(shí)戰(zhàn)下
- mall整合SpringSecurity及JWT實(shí)現(xiàn)認(rèn)證授權(quán)實(shí)戰(zhàn)
- Spring?Security使用數(shù)據(jù)庫登錄認(rèn)證授權(quán)
- Java Spring Security認(rèn)證與授權(quán)及注銷和權(quán)限控制篇綜合解析
- SpringSecurity數(shù)據(jù)庫進(jìn)行認(rèn)證和授權(quán)的使用
- SpringBoot+SpringSecurity實(shí)現(xiàn)基于真實(shí)數(shù)據(jù)的授權(quán)認(rèn)證
- Spring Security OAuth2認(rèn)證授權(quán)示例詳解
- Spring Security實(shí)現(xiàn)身份認(rèn)證和授權(quán)的示例代碼
相關(guān)文章
Java中的Vector和ArrayList區(qū)別及比較
這篇文章主要介紹了Java中的Vector和ArrayList區(qū)別及比較,本文從API、同步、數(shù)據(jù)增長、使用模式4個(gè)方面總結(jié)了它們之間的不同之處,需要的朋友可以參考下2015-03-03JAVA學(xué)習(xí)筆記:注釋、變量的聲明和定義操作實(shí)例分析
這篇文章主要介紹了JAVA學(xué)習(xí)筆記:注釋、變量的聲明和定義操作,結(jié)合實(shí)例形式分析了Java注釋、變量的聲明和定義相關(guān)原理、實(shí)現(xiàn)方法及操作注意事項(xiàng),需要的朋友可以參考下2020-04-04解析SpringBoot?搭建基于?MinIO?的高性能存儲服務(wù)的問題
Minio是Apache?License?v2.0下發(fā)布的對象存儲服務(wù)器,使用MinIO構(gòu)建用于機(jī)器學(xué)習(xí),分析和應(yīng)用程序數(shù)據(jù)工作負(fù)載的高性能基礎(chǔ)架構(gòu)。這篇文章主要介紹了SpringBoot?搭建基于?MinIO?的高性能存儲服務(wù),需要的朋友可以參考下2022-03-03Springboot Maven打包跳過測試的五種方式小結(jié)
本文主要介紹了Springboot Maven打包跳過測試的五種方式小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04