SpringBoot整合SpringSecurity認(rèn)證與授權(quán)
嘮嗑部分
在項(xiàng)目開發(fā)中,權(quán)限認(rèn)證是很重要的,尤其是一些管理類的系統(tǒng),對于權(quán)限要求更為嚴(yán)格,那么在Java開發(fā)中,常用的權(quán)限框架有哪些呢?
推薦的有兩種,Shiro 與 SpringSecurity,當(dāng)然也可以結(jié)合切面自己實(shí)現(xiàn)
Shiro是Apache開源的一款權(quán)限框架,比較輕量級,簡單容易學(xué),但是不能在其中注入Spring中的容器Bean
SpringSecurity是Spring生態(tài)中的一個組件,比較重量級,它也整合了OAuth2協(xié)議,對于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之后,請求其余需要認(rèn)證的接口時,再請求頭攜帶token訪問。
3、服務(wù)器編寫過濾器,對請求頭中的token進(jìn)行驗(yàn)證,判斷用戶登錄是否有效,于此同時,判斷token有效期是否即將過期,如果即將過期,重新頒發(fā)token,如果已過期,返回401未認(rèn)證狀態(tài)碼。
上面說到判斷token有效期是否即將過期,說明一下哈
oauth2中是頒發(fā)了兩個token,一個access_token(訪問token),一個refresh_token(刷新token),刷新token的有效期是訪問token的2倍,如果訪問token過期,就拿刷新token重新申請?jiān)L問token
我們只有一個token,邏輯是判斷token的有效期,如果有效期小于15分鐘,就刷新token,這樣的話既可以實(shí)現(xiàn)toekn的無感知刷新
實(shí)際的token是比較長的一段字符串,標(biāo)準(zhǔn)的jwt token包括頭部、載荷、簽名,格式如下
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWQiOiIxIiwiYXV0aGVudGljYXRpb25zIjpbImFkbWluIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE2ODg2MjI3MjMsImV4cCI6MTY4ODYyNDUyM30.xPRul6ePE1bwSe70rbo0-jPFUxU9O9MPQf9gliZ18X8
接口設(shè)計(jì)說明:
1、用戶認(rèn)證處理器
接口地址:/auth/login
請求方式:POST
請求頭:content-type: application/json;charset=utf-8
接口功能說明:進(jìn)行用戶認(rèn)證,頒發(fā)token,實(shí)際的token比較長,我們是生成了一個字符串充當(dāng)token,實(shí)際token存在于redis中
接口參數(shù):
{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->"password": "","username": ""}出參說明:
tokenInfo:token信息,包括token的失效時間、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
請求方式:POST
請求頭: 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、測試接口
/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ù)時
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 {
// 請求授權(quán)管理
http.authorizeRequests()
// 其他的請求都需要授權(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的本地存儲,做無狀態(tài)登錄
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 注入認(rèn)證管理器
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
6、登錄認(rèn)證邏輯
這一塊有兩個邏輯
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、測試
使用user角色登錄

使用token分別訪問測試接口

使用admin角色登錄

使用token分別訪問測試接口

token令牌檢查接口測試

查看真實(shí)token,在redis存儲

到此這篇關(guān)于SpringBoot整合SpringSecurity認(rèn)證與授權(quán)的文章就介紹到這了,更多相關(guān)SpringBoot SpringSecurity認(rèn)證與授權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(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ì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-11-11
Java兩種方式實(shí)現(xiàn)動態(tài)代理
Java 在 java.lang.reflect 包中有自己的代理支持,該類(Proxy.java)用于動態(tài)生成代理類,只需傳入目標(biāo)接口、目標(biāo)接口的類加載器以及 InvocationHandler 便可為目標(biāo)接口生成代理類及代理對象。我們稱這個Java技術(shù)為:動態(tài)代理2020-10-10
劍指Offer之Java算法習(xí)題精講數(shù)組與列表的查找及字符串轉(zhuǎn)換
跟著思路走,之后從簡單題入手,反復(fù)去看,做過之后可能會忘記,之后再做一次,記不住就反復(fù)做,反復(fù)尋求思路和規(guī)律,慢慢積累就會發(fā)現(xiàn)質(zhì)的變化2022-03-03
logback 實(shí)現(xiàn)給變量指定默認(rèn)值
這篇文章主要介紹了logback 實(shí)現(xiàn)給變量指定默認(rèn)值操作,具有很好的參考家價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
Java語言實(shí)現(xiàn)簡單FTP軟件 FTP遠(yuǎn)程文件管理模塊實(shí)現(xiàn)(10)
這篇文章主要為大家詳細(xì)介紹了Java語言實(shí)現(xiàn)簡單FTP軟件,F(xiàn)TP遠(yuǎn)程文件管理模塊的實(shí)現(xiàn)方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
SpringBoot整合RabbitMQ處理死信隊(duì)列和延遲隊(duì)列
這篇文章將通過示例為大家詳細(xì)介紹SpringBoot整合RabbitMQ時如何處理死信隊(duì)列和延遲隊(duì)列,文中的示例代碼講解詳細(xì),需要的可以參考一下2022-05-05
MyBatis?Generator?ORM層面的代碼自動生成器(推薦)
Mybatis?Generator是一個專門為?MyBatis和?ibatis框架使用者提供的代碼生成器,也可以快速的根據(jù)數(shù)據(jù)表生成對應(yīng)的pojo類、Mapper接口、Mapper文件,甚至生成QBC風(fēng)格的查詢對象,這篇文章主要介紹了MyBatis?Generator?ORM層面的代碼自動生成器,需要的朋友可以參考下2023-01-01

