SpringBoot整合SpringSecurity認(rèn)證與授權(quán)
嘮嗑部分
在項(xiàng)目開(kāi)發(fā)中,權(quán)限認(rèn)證是很重要的,尤其是一些管理類的系統(tǒng),對(duì)于權(quán)限要求更為嚴(yán)格,那么在Java開(kāi)發(fā)中,常用的權(quán)限框架有哪些呢?
推薦的有兩種,Shiro 與 SpringSecurity,當(dāng)然也可以結(jié)合切面自己實(shí)現(xiàn)
Shiro是Apache開(kāi)源的一款權(quán)限框架,比較輕量級(jí),簡(jiǎn)單容易學(xué),但是不能在其中注入Spring中的容器Bean
SpringSecurity是Spring生態(tài)中的一個(gè)組件,比較重量級(jí),它也整合了OAuth2協(xié)議,對(duì)于Spring框架來(lái)說(shuō),更推薦SpringSecurity
今天我們就來(lái)分享一下如何整合SpringSecurity進(jìn)行認(rèn)證與授權(quán),順便實(shí)現(xiàn)一下token的無(wú)感知續(xù)期
SpringSecurity進(jìn)行認(rèn)證與授權(quán)是SpringSecurity框架進(jìn)行處理,我們就不必多說(shuō),按照步驟進(jìn)行編碼就OK了
token的無(wú)感知續(xù)期我們來(lái)說(shuō)一下思路:
1、用戶在登錄成功后,由服務(wù)器下發(fā)token,有效期30分鐘。
2、客戶端拿到token之后,請(qǐng)求其余需要認(rèn)證的接口時(shí),再請(qǐng)求頭攜帶token訪問(wèn)。
3、服務(wù)器編寫過(guò)濾器,對(duì)請(qǐng)求頭中的token進(jìn)行驗(yàn)證,判斷用戶登錄是否有效,于此同時(shí),判斷token有效期是否即將過(guò)期,如果即將過(guò)期,重新頒發(fā)token,如果已過(guò)期,返回401未認(rèn)證狀態(tài)碼。
上面說(shuō)到判斷token有效期是否即將過(guò)期,說(shuō)明一下哈
oauth2中是頒發(fā)了兩個(gè)token,一個(gè)access_token(訪問(wèn)token),一個(gè)refresh_token(刷新token),刷新token的有效期是訪問(wèn)token的2倍,如果訪問(wèn)token過(guò)期,就拿刷新token重新申請(qǐng)?jiān)L問(wèn)token
我們只有一個(gè)token,邏輯是判斷token的有效期,如果有效期小于15分鐘,就刷新token,這樣的話既可以實(shí)現(xiàn)toekn的無(wú)感知刷新
實(shí)際的token是比較長(zhǎng)的一段字符串,標(biāo)準(zhǔn)的jwt token包括頭部、載荷、簽名,格式如下
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWQiOiIxIiwiYXV0aGVudGljYXRpb25zIjpbImFkbWluIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE2ODg2MjI3MjMsImV4cCI6MTY4ODYyNDUyM30.xPRul6ePE1bwSe70rbo0-jPFUxU9O9MPQf9gliZ18X8
接口設(shè)計(jì)說(shuō)明:
1、用戶認(rèn)證處理器
接口地址:/auth/login
請(qǐng)求方式:POST
請(qǐng)求頭:content-type: application/json;charset=utf-8
接口功能說(shuō)明:進(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": ""}出參說(shuō)明:
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
接口功能說(shuō)明:token令牌檢查
出參說(shuō)明:
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角色才能訪問(wèn)
/common/test:任何角色都可以訪問(wèn)
言歸正傳
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過(guò)濾器的編寫
/*
* @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,如果在核心配置文件中配置了白名單,則跳過(guò)驗(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)證信息已過(guò)期"));
return;
}
if (jwtUtil.validTokenIssued(tokenStr)) {
response(response, BaseResult.error().setCode(ResponseStateConstant.NO_LOGIN).setMsg("用戶認(rèn)證信息已過(guò)期"));
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)過(guò)濾器
.addFilterBefore(tokenVerificationFilter, UsernamePasswordAuthenticationFilter.class)
// 禁用springsecurity的本地存儲(chǔ),做無(wú)狀態(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分別訪問(wèn)測(cè)試接口

使用admin角色登錄

使用token分別訪問(wèn)測(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ù)庫(kù)進(jìn)行認(rèn)證和授權(quán)的使用
- SpringSecurity授權(quán)機(jī)制的實(shí)現(xiàn)(AccessDecisionManager與投票決策)
相關(guān)文章
SpringBoot發(fā)送郵件功能 驗(yàn)證碼5分鐘過(guò)期
這篇文章主要為大家詳細(xì)介紹了SpringBoot發(fā)送郵件功能,驗(yàn)證碼5分鐘過(guò)期,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03
java抓取網(wǎng)頁(yè)或文件中的郵箱號(hào)碼
這篇文章主要為大家詳細(xì)介紹了java如何抓取網(wǎng)頁(yè)或文件中的郵箱號(hào)碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
springboot的異步任務(wù):無(wú)返回值和有返回值問(wèn)題
這篇文章主要介紹了springboot的異步任務(wù):無(wú)返回值和有返回值問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07

