SpringBoot整合SpringSecurity認(rèn)證與授權(quán)
嘮嗑部分
在項(xiàng)目開發(fā)中,權(quán)限認(rèn)證是很重要的,尤其是一些管理類的系統(tǒng),對(duì)于權(quán)限要求更為嚴(yán)格,那么在Java開發(fā)中,常用的權(quán)限框架有哪些呢?
推薦的有兩種,Shiro 與 SpringSecurity,當(dāng)然也可以結(jié)合切面自己實(shí)現(xiàn)
Shiro是Apache開源的一款權(quán)限框架,比較輕量級(jí),簡(jiǎn)單容易學(xué),但是不能在其中注入Spring中的容器Bean
SpringSecurity是Spring生態(tài)中的一個(gè)組件,比較重量級(jí),它也整合了OAuth2協(xié)議,對(duì)于Spring框架來說,更推薦SpringSecurity
今天我們就來分享一下如何整合SpringSecurity進(jìn)行認(rèn)證與授權(quán),順便實(shí)現(xiàn)一下token的無感知續(xù)期
SpringSecurity進(jìn)行認(rèn)證與授權(quán)是SpringSecurity框架進(jìn)行處理,我們就不必多說,按照步驟進(jìn)行編碼就OK了
token的無感知續(xù)期我們來說一下思路:
1、用戶在登錄成功后,由服務(wù)器下發(fā)token,有效期30分鐘。
2、客戶端拿到token之后,請(qǐng)求其余需要認(rèn)證的接口時(shí),再請(qǐng)求頭攜帶token訪問。
3、服務(wù)器編寫過濾器,對(duì)請(qǐng)求頭中的token進(jìn)行驗(yàn)證,判斷用戶登錄是否有效,于此同時(shí),判斷token有效期是否即將過期,如果即將過期,重新頒發(fā)token,如果已過期,返回401未認(rèn)證狀態(tài)碼。
上面說到判斷token有效期是否即將過期,說明一下哈
oauth2中是頒發(fā)了兩個(gè)token,一個(gè)access_token(訪問token),一個(gè)refresh_token(刷新token),刷新token的有效期是訪問token的2倍,如果訪問token過期,就拿刷新token重新申請(qǐng)?jiān)L問token
我們只有一個(gè)token,邏輯是判斷token的有效期,如果有效期小于15分鐘,就刷新token,這樣的話既可以實(shí)現(xiàn)toekn的無感知刷新
實(shí)際的token是比較長(zhǎng)的一段字符串,標(biāo)準(zhǔn)的jwt token包括頭部、載荷、簽名,格式如下
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWQiOiIxIiwiYXV0aGVudGljYXRpb25zIjpbImFkbWluIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE2ODg2MjI3MjMsImV4cCI6MTY4ODYyNDUyM30.xPRul6ePE1bwSe70rbo0-jPFUxU9O9MPQf9gliZ18X8
接口設(shè)計(jì)說明:
1、用戶認(rèn)證處理器
接口地址:/auth/login
請(qǐng)求方式:POST
請(qǐng)求頭:content-type: application/json;charset=utf-8
接口功能說明:進(jìn)行用戶認(rèn)證,頒發(fā)token,實(shí)際的token比較長(zhǎng),我們是生成了一個(gè)字符串充當(dāng)token,實(shí)際token存在于redis中
接口參數(shù):
{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->"password": "","username": ""}
出參說明:
tokenInfo:token信息,包括token的失效時(shí)間、token值、用戶信息等等
user:用戶信息
{ "code": 200, "data": { "tokenInfo": { "expirationTime": "2023-07-06 12:25:42", "token": "3f49e86b93c743f2865a4446a7a85398", "user": { "authentications": [ "ROLE_user" ], "id": "2", "username": "user" } }, "user": { "roles": [ "ROLE_user" ], "userId": "2", "userStatus": 1, "userType": 0, "username": "admin" } }, "msg": "登陸成功" }
2、用戶令牌檢查處理器
接口地址:/auth/checkToken
請(qǐng)求方式:POST
請(qǐng)求頭: X-Access-Token
接口功能說明:token令牌檢查
出參說明:
status: 檢查結(jié)果
token:token信息
{ "code": 200, "data": { "status": true, "token": { "expirationTime": "2023-07-06 12:25:42", "token": "3f49e86b93c743f2865a4446a7a85398", "user": { "authentications": [ "ROLE_user" ], "id": "2", "username": "user" } } }, "msg": "操作成功" }
3、測(cè)試接口
/admin/common/test:只有admin角色才能訪問
/common/test:任何角色都可以訪問
言歸正傳
1、相關(guān)SQL腳本
create database `springsecurity_case` character set 'utf8mb4'; use `springsecurity_case`; create table t_user( user_id varchar(50) primary key comment '用戶id', username varchar(50) not null comment '用戶名', password varchar(100) not null comment '密碼', role varchar(50) not null comment '角色', user_status tinyint(1) default 1 comment '用戶狀態(tài)' ); insert into t_user values ('1', 'admin', '$2a$10$wmUXgiTZzc3ux3h3UiuxWumeDYbt8uaZmmPw6utx9GyyuGEDSTNJy', 'admin', 1), ('2', 'user', '$2a$10$wmUXgiTZzc3ux3h3UiuxWumeDYbt8uaZmmPw6utx9GyyuGEDSTNJy', 'user', 1);
2、創(chuàng)建項(xiàng)目&導(dǎo)入依賴
<!-- ... 常規(guī)化的依賴省略了--> <!-- redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- springSecurity--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
3、UserDetailService實(shí)現(xiàn)類的編寫,認(rèn)證的主邏輯
/* * @Project:springboot-springsecurity-case * @Author:cxs * @Motto:放下雜念,只為迎接明天更好的自己 * */ @Service @Slf4j public class UserDetailServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (!StringUtils.hasLength(username)) { throw new UsernameNotFoundException("用戶名豈能為空!"); } QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); User user = userMapper.selectOne(queryWrapper); if (ObjectUtils.isEmpty(user)) { throw new UsernameNotFoundException("用戶名不存在"); } if (user.getUserStatus().equals(2)) { throw new LockedException("賬戶已被鎖定,認(rèn)證失敗"); } GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + user.getRole()); Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>(); grantedAuthorities.add(grantedAuthority); return new AuthUser(user.getUserId(), username, user.getPassword(), grantedAuthorities); } }
4、Token過濾器的編寫
/* * @Project:springboot-springsecurity-case * @Author:cxs * @Motto:放下雜念,只為迎接明天更好的自己 * */ @Component public class TokenVerificationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private CommonConfig commonConfig; @Autowired private RedisUtil redisUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 獲取url,如果在核心配置文件中配置了白名單,則跳過驗(yàn)證 String requestURI = request.getRequestURI(); if (ignore(requestURI)) { filterChain.doFilter(request, response); return; } // 獲取X-Access-Token String header = request.getHeader(CommonContent.TOKEN); if (!StringUtils.hasLength(header)) { filterChain.doFilter(request, response); return; } Token token = null; List<String> strings = null; try { // 根據(jù)X-Access-Token去redis查詢真實(shí)token String tokenStr = redisUtil.getString(redisUtil.getCacheKey(CachePrefixContent.TOKEN_PREFIX, header.trim())); if (!StringUtils.hasLength(tokenStr)) { response(response, BaseResult.error().setCode(ResponseStateConstant.NO_LOGIN).setMsg("用戶認(rèn)證信息已過期")); return; } if (jwtUtil.validTokenIssued(tokenStr)) { response(response, BaseResult.error().setCode(ResponseStateConstant.NO_LOGIN).setMsg("用戶認(rèn)證信息已過期")); return; } // 校驗(yàn)信息是否正確,省略 token = jwtUtil.parseToken(tokenStr); strings = token.getUser().getAuthentications(); Authentication context = new UsernamePasswordAuthenticationToken( token.getUser().getUsername(), token.getUser().getUsername(), AuthorityUtils.createAuthorityList(strings.toArray(new String[0])) ); SecurityContextHolder.getContext().setAuthentication(context); Long expire = redisUtil.getExpire(redisUtil.getCacheKey(CachePrefixContent.TOKEN_PREFIX, header)); // 有效期小于15分鐘,續(xù)時(shí) if (expire <= 900L) { redisUtil.set(redisUtil.getCacheKey(CachePrefixContent.TOKEN_PREFIX, header), jwtUtil.generateToken(token.getUser()), commonConfig.getValidityTime(), TimeUnit.MINUTES); } filterChain.doFilter(request, response); } catch (Exception e) { if (e instanceof SignatureException) { BaseResult error = BaseResult.error(); error.setMsg(CurrencyErrorEnum.UNAUTHORIZED.getMsg()); error.setCode(CurrencyErrorEnum.UNAUTHORIZED.getCode()); response(response, error); } else if (e instanceof ExpiredJwtException) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); BaseResult error = BaseResult.error(); error.setMsg(CurrencyErrorEnum.UNAUTHORIZED_BE_OVERDUE.getMsg()); error.setCode(CurrencyErrorEnum.UNAUTHORIZED_BE_OVERDUE.getCode()); response(response, error); } else if (e instanceof JwtException) { BaseResult error = BaseResult.error(); error.setMsg(CurrencyErrorEnum.UNAUTHORIZED.getMsg()); error.setCode(CurrencyErrorEnum.UNAUTHORIZED.getCode()); response(response, error); } else { throw e; } } } // ... }
5、Security核心配置文件編寫
/* * @Project:springboot-springsecurity-case * @Author:cxs * @Motto:放下雜念,只為迎接明天更好的自己 * */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /* 創(chuàng)建加密編碼器 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private TokenVerificationFilter tokenVerificationFilter; @Autowired private AccessForbiddenHandler forbiddenHandler; @Autowired private AuthenticationHandler authenticationHandler; @Autowired private CommonConfig commonConfig; @Override protected void configure(HttpSecurity http) throws Exception { // 請(qǐng)求授權(quán)管理 http.authorizeRequests() // 其他的請(qǐng)求都需要授權(quán) .antMatchers(commonConfig.getIgnoreUrl()).permitAll() .antMatchers("/common/**").hasAnyRole("admin", "user") .antMatchers("/admin/**").hasRole("admin") .anyRequest().authenticated() .and() .exceptionHandling() .accessDeniedHandler(forbiddenHandler) .authenticationEntryPoint(authenticationHandler) .and() .csrf().disable() // 整合token校驗(yàn)過濾器 .addFilterBefore(tokenVerificationFilter, UsernamePasswordAuthenticationFilter.class) // 禁用springsecurity的本地存儲(chǔ),做無狀態(tài)登錄 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } /** * 注入認(rèn)證管理器 * @return * @throws Exception */ @Bean public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } }
6、登錄認(rèn)證邏輯
這一塊有兩個(gè)邏輯
1、首先如果用戶帶著X-Access-Token進(jìn)行登錄,首先判斷是否有效,如果有效的話,直接將之前的token回傳給用戶
2、如果失效、或者用戶未帶X-Access-Token,進(jìn)行登錄邏輯
public void login(UserLoginDTO dto, HttpServletRequest request, HttpServletResponse response, BaseResult result) { // 判斷用戶是否攜帶X-Access-Token String accessTokenKey = request.getHeader(CommonContent.TOKEN); if (StringUtils.hasLength(accessTokenKey)) { String tokenStr = redisUtil.getString(redisUtil.getCacheKey(CachePrefixContent.TOKEN_PREFIX, accessTokenKey.trim())); Token token = null; try { token = jwtUtil.parseToken(tokenStr); } catch (Exception e) { log.info("用戶登錄:用戶已有token校驗(yàn)失敗"); } // 進(jìn)行token驗(yàn)證 if (!ObjectUtils.isEmpty(token)) { String generateToken = jwtUtil.generateToken(token.getUser()); redisUtil.set(redisUtil.getCacheKey(CachePrefixContent.TOKEN_PREFIX, accessTokenKey), generateToken, commonConfig.getValidityTime(), TimeUnit.MINUTES); Token parseToken = jwtUtil.parseToken(generateToken); if (!ObjectUtils.isEmpty(parseToken)) parseToken.setToken(accessTokenKey); UserLoginVO vo = new UserLoginVO(); vo.setTokenInfo(parseToken); UserVO userVO = new UserVO(); User userInfo = userMapper.selectById(parseToken.getUser().getId()); BeanUtils.copyProperties(userInfo, userVO); userVO.setRoles(parseToken.getUser().getAuthentications()); vo.setUser(userVO); result.setCode(ResponseStateConstant.OPERA_SUCCESS).setData(vo).setMsg("登陸成功"); return; } } // 如果失效,或者未帶X-Access-Token,進(jìn)行登錄邏輯 String password = dto.getPassword().trim(); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(dto.getUsername().trim(), password); Authentication authenticate = null; try { authenticate = authenticationManager.authenticate(token); } catch (AuthenticationException e) { e.printStackTrace(); result.setCode(ResponseStateConstant.OPERA_FAIL).setMsg(e.getMessage()); } if (authenticate != null) { UserLoginVO vo = new UserLoginVO(); SecurityContextHolder.getContext().setAuthentication(authenticate); Object principal = authenticate.getPrincipal(); AuthUser user = (AuthUser) principal; List<String> auths = CollectionUtils.isEmpty(user.getAuthorities()) ? new ArrayList<>(0) : user.getAuthorities().stream().map(a -> a.getAuthority()).collect(Collectors.toList()); // 用戶登陸成功,生成token String tokenStr = IdUtil.simpleUUID(); String generateToken = jwtUtil.generateToken(UserSubject.builder() .id(user.getId()) .username(user.getUsername()) .authentications(auths).build()); redisUtil.set(redisUtil.getCacheKey(CachePrefixContent.TOKEN_PREFIX, tokenStr), generateToken, commonConfig.getValidityTime(), TimeUnit.MINUTES); Token parseToken = jwtUtil.parseToken(generateToken); if (!ObjectUtils.isEmpty(parseToken)) parseToken.setToken(tokenStr); vo.setTokenInfo(parseToken); UserVO userVO = new UserVO(); User userInfo = userMapper.selectById(user.getId()); BeanUtils.copyProperties(userInfo, userVO); userVO.setRoles(auths); vo.setUser(userVO); result.setCode(ResponseStateConstant.OPERA_SUCCESS).setData(vo).setMsg("登陸成功"); } }
7、測(cè)試
使用user角色登錄
使用token分別訪問測(cè)試接口
使用admin角色登錄
使用token分別訪問測(cè)試接口
token令牌檢查接口測(cè)試
查看真實(shí)token,在redis存儲(chǔ)
到此這篇關(guān)于SpringBoot整合SpringSecurity認(rèn)證與授權(quán)的文章就介紹到這了,更多相關(guān)SpringBoot SpringSecurity認(rèn)證與授權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity實(shí)現(xiàn)權(quán)限認(rèn)證與授權(quán)的使用示例
- SpringSecurity進(jìn)行認(rèn)證與授權(quán)的示例代碼
- springSecurity用戶認(rèn)證和授權(quán)的實(shí)現(xiàn)
- 深入淺析springsecurity入門登錄授權(quán)
- SpringSecurityOAuth2實(shí)現(xiàn)微信授權(quán)登錄
- SpringBoot+SpringSecurity實(shí)現(xiàn)基于真實(shí)數(shù)據(jù)的授權(quán)認(rèn)證
- springsecurity第三方授權(quán)認(rèn)證的項(xiàng)目實(shí)踐
- SpringSecurity數(shù)據(jù)庫進(jìn)行認(rèn)證和授權(quán)的使用
- SpringSecurity授權(quán)機(jī)制的實(shí)現(xiàn)(AccessDecisionManager與投票決策)
相關(guān)文章
springboot整合vue實(shí)現(xiàn)上傳下載文件
這篇文章主要為大家詳細(xì)介紹了springboot整合vue實(shí)現(xiàn)上傳下載文件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11Java兩種方式實(shí)現(xiàn)動(dòng)態(tài)代理
Java 在 java.lang.reflect 包中有自己的代理支持,該類(Proxy.java)用于動(dòng)態(tài)生成代理類,只需傳入目標(biāo)接口、目標(biāo)接口的類加載器以及 InvocationHandler 便可為目標(biāo)接口生成代理類及代理對(duì)象。我們稱這個(gè)Java技術(shù)為:動(dòng)態(tài)代理2020-10-10劍指Offer之Java算法習(xí)題精講數(shù)組與列表的查找及字符串轉(zhuǎn)換
跟著思路走,之后從簡(jiǎn)單題入手,反復(fù)去看,做過之后可能會(huì)忘記,之后再做一次,記不住就反復(fù)做,反復(fù)尋求思路和規(guī)律,慢慢積累就會(huì)發(fā)現(xiàn)質(zhì)的變化2022-03-03logback 實(shí)現(xiàn)給變量指定默認(rèn)值
這篇文章主要介紹了logback 實(shí)現(xiàn)給變量指定默認(rèn)值操作,具有很好的參考家價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08Java語言實(shí)現(xiàn)簡(jiǎn)單FTP軟件 FTP遠(yuǎn)程文件管理模塊實(shí)現(xiàn)(10)
這篇文章主要為大家詳細(xì)介紹了Java語言實(shí)現(xiàn)簡(jiǎn)單FTP軟件,F(xiàn)TP遠(yuǎn)程文件管理模塊的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04SpringBoot整合RabbitMQ處理死信隊(duì)列和延遲隊(duì)列
這篇文章將通過示例為大家詳細(xì)介紹SpringBoot整合RabbitMQ時(shí)如何處理死信隊(duì)列和延遲隊(duì)列,文中的示例代碼講解詳細(xì),需要的可以參考一下2022-05-05MyBatis?Generator?ORM層面的代碼自動(dòng)生成器(推薦)
Mybatis?Generator是一個(gè)專門為?MyBatis和?ibatis框架使用者提供的代碼生成器,也可以快速的根據(jù)數(shù)據(jù)表生成對(duì)應(yīng)的pojo類、Mapper接口、Mapper文件,甚至生成QBC風(fēng)格的查詢對(duì)象,這篇文章主要介紹了MyBatis?Generator?ORM層面的代碼自動(dòng)生成器,需要的朋友可以參考下2023-01-01