生活随笔
收集整理的這篇文章主要介紹了
Security+jwt+验证码实现验证和授权
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
微服務(wù)Security+jwt+驗證碼實現(xiàn)認證和授權(quán)
簡要介紹
本次博客采用Spring Security、jwt、驗證碼的形式實現(xiàn)登錄驗證,項目本身是一個前后端分離項目。如果你的項目在登陸時不需要驗證碼,你只需要在后續(xù)的代碼中,將有關(guān)驗證碼的過濾器刪除。
gitee倉庫連接
基本流程
1、前端請求后端"/captcha"驗證碼接口,后端生成驗證碼文本及編碼并將其存入redis緩存,然后返回驗證碼文本(五個字符)和驗證碼base64編碼給前端。
2、前端顯示驗證碼圖片,用戶輸入用戶名、密碼、驗證碼點擊登錄。
3、后端開啟驗證
(1)開啟驗證碼驗證,走驗證碼過濾器,如果正確則放行走下一個過濾器,如果錯誤則拋出異常給登錄失敗過濾器,返回失敗信息給前端。
(2)開啟jwt驗證。
a:如果請求沒有攜帶token,則認為是首次登錄,jwt過濾器不做任何事情,放行走UsernamePasswordAuthenticationFilter過濾器,該過濾器會通過查數(shù)據(jù)庫驗證用戶的身份信息決定用戶是否能登錄。如果驗證成功會生成一個Authentication,并保存在SecurityContext(security上下文)中。Authentication包含用戶的信息及權(quán)限。
b:如果請求中攜帶了token,走jwt過濾器,過濾器判斷jwt是否為空、攜帶信息(用戶名)是否為空,jwt是否過期,如果上述條件都正常,創(chuàng)建一個Authentication的實現(xiàn)類對象,并通過自定義的獲取用戶權(quán)限方法獲取權(quán)限,然后通過userDetailService的loadUserByUsername方法得到UserDetails對象,里面包含用戶信息和權(quán)限,調(diào)用Authentication的setUserDetails方法,最后將該Authentication對象存入到Security上下文中,后續(xù)的過濾器查詢到該Authentication,就會直接放行,比如UsernamePasswordAuthenticationFilter過濾器。
上述認證都是由過濾器完成,因為認證是有順序的,所以在security配置文件中我們要設(shè)置這三個過濾器的順序為:驗證碼過濾器=》jwt過濾器=》UsernamePasswordAuthenticationFilter
.addFilterBefore(captchaFilter
,UsernamePasswordAuthenticationFilter.class)
.addFilterAt(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(a,b)默認會將a設(shè)置在b之前。
至此登錄認證就結(jié)束了,需要注意的是,我們在認證的時候已經(jīng)將用戶的權(quán)限列表加入到了Authentication并放在了Security上下文中,所以后續(xù)對于資源做權(quán)限判斷時時,只需要再目標接口上加入一下注解實現(xiàn)。@PreAuthorize(“hasAuthority(‘sys:role:list’)”)
@PreAuthorize(“hasRole(‘ROLE_admin’)”)
當請求該接口時,security就會去Authentication中查詢有無該權(quán)限或者該角色。
核心代碼
1、驗證碼過濾器
該過濾器用于驗證驗證碼是否正確。該過濾器繼承的是OncePerRequestFilter,因為每次登錄只需要驗證一次。
package com.komorebi.security;import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.komorebi.common.CaptchaException;
import com.komorebi.common.Const;
import com.komorebi.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component
public class CaptchaFilter extends OncePerRequestFilter {@AutowiredRedisUtil redisUtil
;@AutowiredLoginFailureHandler loginFailureHandler
;@Overrideprotected void doFilterInternal(HttpServletRequest request
, HttpServletResponse response
, FilterChain filterChain
) throws ServletException, IOException {log
.info("開始驗證碼驗證");String url
= request
.getRequestURI();if("/login".equals(url
) && request
.getMethod().equals("POST")){try{validate(request
);}catch (CaptchaException e
){loginFailureHandler
.onAuthenticationFailure(request
,response
,e
);}}filterChain
.doFilter(request
,response
);}private void validate(HttpServletRequest request
) {String key
= request
.getParameter("tokens");String code
= request
.getParameter("code");if(StringUtils.isBlank(key
) || StringUtils.isBlank(code
)){throw new CaptchaException("驗證碼信息為空");}if(!code
.equals(redisUtil
.hget(Const.CAPTCHA_KEY
,key
))){throw new CaptchaException("驗證碼錯誤");}redisUtil
.del(Const.CAPTCHA_KEY
,key
);}
}
2、jwt過濾器
package com.komorebi.security;import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.komorebi.entity.SysUser;
import com.komorebi.mapper.SysUserMapper;
import com.komorebi.service.SysUserService;
import com.komorebi.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {@AutowiredJwtUtils jwtUtils
;@AutowiredUserDetailServiceImpl userDetailService
;@AutowiredSysUserService sysUserService
;public JwtAuthenticationFilter(AuthenticationManager authenticationManager
) {super(authenticationManager
);}@Overrideprotected void doFilterInternal(HttpServletRequest request
, HttpServletResponse response
, FilterChain chain
) throws IOException, ServletException {log
.info("開啟jwt認證");String jwt
= request
.getHeader(jwtUtils
.getHeader());if(jwt
!= null){Claims claim
= jwtUtils
.getClaimByToken(jwt
);if(claim
!= null){String username
= claim
.getSubject();log
.info("jwt認證:檢查用戶名");if(username
!= null && SecurityContextHolder.getContext().getAuthentication() == null){SysUser sysUser
= sysUserService
.getByUserName(username
);Long userId
= sysUser
.getId();UserDetails userDetails
= userDetailService
.loadUserByUsername(username
);if(!jwtUtils
.isTokenExpired(claim
)){UsernamePasswordAuthenticationToken auth
= new UsernamePasswordAuthenticationToken(username
, null, userDetailService
.getUserAuthority(userId
));auth
.setDetails(userDetails
);log
.info("通過jwt認證,設(shè)置Authentication,后續(xù)過濾器放行");SecurityContextHolder.getContext().setAuthentication(auth
);}}}}else {log
.info("首次登陸 jwt為空");}chain
.doFilter(request
,response
);}
}
由于該過濾器我們繼承于BasicAuthenticationFilter,也可以繼承BasicAuthenticationFilter 類,并且重寫了構(gòu)造函數(shù),所以在SecurityConfig中要采用Bean注入。對應(yīng)SecurityConfig文件
@BeanJwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {return new JwtAuthenticationFilter(authenticationManager());}
3、UserDetailServiceImpl
UsernamePasswordAuthenticationFilter主要功能為:用戶登錄信息驗證,獲取用戶權(quán)限,并將上述信息封裝為UserDetails,然后生成Authentication,將UserDetails加入到Authentication中,最終將Authentication加入到security上下文中。
封裝為UserDetails的功能是通過UserDetailService實現(xiàn)的,因為UserDetailService是接口,所以定義UserDetailServiceImpl實現(xiàn)該接口,即UserDetailServiceImpl用于驗證用戶信息和獲取用用戶權(quán)限。
package com.komorebi.security;import com.komorebi.entity.SysUser;
import com.komorebi.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class UserDetailServiceImpl implements UserDetailsService {@AutowiredSysUserService userService
;@Overridepublic UserDetails loadUserByUsername(String username
) throws UsernameNotFoundException {log
.info("開始登陸驗證,用戶名為: {}",username
);SysUser user
= userService
.getByUserName(username
);if(user
== null){log
.info("用戶名或密碼不正確");throw new UsernameNotFoundException("用戶名或密碼不正確");}return new User(user
.getUsername(),user
.getPassword(),getUserAuthority(user
.getId()));}public List<GrantedAuthority> getUserAuthority(Long userId
){String authority
= userService
.getUserAuthority(userId
);return AuthorityUtils.commaSeparatedStringToAuthorityList(authority
);}
}
4、登陸成功過濾器
package com.komorebi.security;import cn.hutool.json.JSONUtil;
import com.komorebi.common.Result;
import com.komorebi.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Componentpublic class LoginSuccessHandler implements AuthenticationSuccessHandler{@AutowiredJwtUtils jwtUtils
;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request
, HttpServletResponse response
, Authentication authentication
) throws IOException, ServletException {response
.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream
= response
.getOutputStream();String username
= authentication
.getName();String jwt
= jwtUtils
.generateToken(username
);response
.setHeader(jwtUtils
.getHeader(), jwt
);Result result
= Result.success("登陸成功過濾器執(zhí)行");outputStream
.write(JSONUtil.toJsonStr(result
).getBytes("UTF-8"));outputStream
.flush();outputStream
.close();}
}
這里,我們過濾器需要向前端傳遞json數(shù)據(jù),但是security是不支持return json數(shù)據(jù)的,所以我們只能通過流的方式返回數(shù)據(jù)。
基本步驟:
(1)獲取reponse的字節(jié)輸出流
(2)創(chuàng)建返回對象
(3)將對象轉(zhuǎn)為字節(jié)數(shù)組輸出給response
(4)刷新緩沖區(qū),關(guān)閉流
后續(xù)的過濾器只要涉及到返回數(shù)據(jù)給前端,都會使用該方法。
5、登陸失敗過濾器
package com.komorebi.security;import cn.hutool.json.JSONUtil;
import com.komorebi.common.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request
, HttpServletResponse response
, AuthenticationException exception
) throws IOException, ServletException {response
.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream
= response
.getOutputStream();Result result
= Result.fail(exception
.getMessage());outputStream
.write(JSONUtil.toJsonStr(result
).getBytes("UTF-8"));outputStream
.flush();outputStream
.close();}
}
6、登出成功過濾器
該處理器會返還給前端一個空的jwt,即前端下次請求時jwt為空,代表未登錄。如果是將jwt存在redis中,還要清除緩存。
package com.komorebi.security;import cn.hutool.json.JSONUtil;
import com.komorebi.common.Const;
import com.komorebi.common.Result;
import com.komorebi.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {@AutowiredJwtUtils jwtUtils
;@Overridepublic void onLogoutSuccess(HttpServletRequest request
, HttpServletResponse response
, Authentication authentication
) throws IOException, ServletException {System.out
.println("退出成功處理器");if(authentication
!= null){new SecurityContextLogoutHandler().logout(request
,response
,authentication
);}response
.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream
= response
.getOutputStream();response
.setHeader(jwtUtils
.getHeader(),"");Result result
= Result.success("登出成功");outputStream
.write(JSONUtil.toJsonStr(result
).getBytes("UTF-8"));outputStream
.flush();outputStream
.close();}
}
7、權(quán)限不足過濾器
該過濾器是用于處理權(quán)限不足時的情況。
package com.komorebi.security;import cn.hutool.json.JSONUtil;
import com.komorebi.common.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request
, HttpServletResponse response
, AccessDeniedException accessDeniedException
) throws IOException, ServletException {response
.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream
= response
.getOutputStream();response
.setStatus(HttpServletResponse.SC_FORBIDDEN
);Result result
= Result.fail(accessDeniedException
.getMessage());outputStream
.write(JSONUtil.toJsonStr(result
).getBytes("UTF-8"));outputStream
.flush();outputStream
.close();}
}
8、未認證過濾器
該過濾器是用于處理用戶未登錄的情況。
package com.komorebi.security;import cn.hutool.json.JSONUtil;
import com.komorebi.common.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request
, HttpServletResponse response
, AuthenticationException authException
) throws IOException, ServletException {response
.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream
= response
.getOutputStream();response
.setStatus(HttpServletResponse.SC_UNAUTHORIZED
);Result result
= Result.fail("請先登錄");outputStream
.write(JSONUtil.toJsonStr(result
).getBytes("UTF-8"));outputStream
.flush();outputStream
.close();}
}
9、JWT工具類
package com.komorebi.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;import java.util.Date;
@Data
@Component
@ConfigurationProperties(prefix
= "komorebi.jwt")
public class JwtUtils {private long expire
;private String secret
;private String header
;public String generateToken(String username
){Date nowDate
= new Date();Date expireDate
= new Date(nowDate
.getTime() * expire
);return Jwts.builder().setHeaderParam("typ","JWT").setSubject(username
).setIssuedAt(nowDate
).setExpiration(expireDate
).signWith(SignatureAlgorithm.HS256
,secret
).compact();}public Claims getClaimByToken(String jwt
){try{return Jwts.parser().setSigningKey(secret
).parseClaimsJws(jwt
).getBody();}catch (Exception e
){return null;}}public boolean isTokenExpired(Claims claims
){return claims
.getExpiration().before(new Date());}
}
expire、secret、header三個變量存放在application.yml文件中
通過該注解@ConfigurationProperties(prefix = “komorebi.jwt”)實現(xiàn)通過配置文件賦值expire、secret、header。
10、SecurityConfig
該配置類會對前面定義的所有過濾器進行配置
package com.komorebi.config;import com.komorebi.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled
= true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@AutowiredLoginSuccessHandler loginSuccessHandler
;@AutowiredLoginFailureHandler loginFailureHandler
;@AutowiredCaptchaFilter captchaFilter
;@AutowiredJwtAccessDeniedHandler jwtAccessDeniedHandler
;@AutowiredJwtAuthenticationEntryPoint jwtAuthenticationEntryPoint
;@AutowiredUserDetailsService userDetailService
;@AutowiredJwtLogoutSuccessHandler jwtLogoutSuccessHandler
;@BeanJwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {return new JwtAuthenticationFilter(authenticationManager());}@BeanBCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();}private static final String [] URL_WHITELIST
= {"/login","/logout","/captcha","/favicon.ico",};@Overrideprotected void configure(AuthenticationManagerBuilder auth
) throws Exception {auth
.userDetailsService(userDetailService
).passwordEncoder(new BCryptPasswordEncoder());}@Overrideprotected void configure(HttpSecurity http
) throws Exception {http
.cors().and().csrf().disable().formLogin().successHandler(loginSuccessHandler
).failureHandler(loginFailureHandler
).and().logout().logoutSuccessHandler(jwtLogoutSuccessHandler
).and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS
).and().authorizeRequests().antMatchers(URL_WHITELIST
).permitAll().anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint
).accessDeniedHandler(jwtAccessDeniedHandler
).and().addFilterBefore(captchaFilter
,UsernamePasswordAuthenticationFilter.class).addFilterAt(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}
}
測試
1、當驗證碼錯誤時
2、驗證碼、密碼正確時(沒有攜帶jwt登錄)
此時會返回token
3、攜帶jwt請求后端接口
@RestController
@RequestMapping("/test")
public class TestController {@AutowiredSysUserService sysUserService
;@GetMapping@PreAuthorize("hasRole('admin')")public Object test(){return Result.success(sysUserService
.list());}
}
/test接口需要用戶具備admin角色,此處登錄的用戶具有該角色,則訪問成功。
4、對于需要某種權(quán)限或者角色才能訪問的后端接口只需要在節(jié)后上面進行設(shè)置
(1)需要權(quán)限才能訪問
@PostMapping("/save")@PreAuthorize("hasAuthority('sys:role:save')")public Result save(@Validated @RequestBody SysRole sysRole
){sysRole
.setCreated(LocalDateTime.now());sysRole
.setStatu(Const.STATUS_ON
);sysRoleService
.save(sysRole
);return Result.success(sysRole
);}
(2)需要用于某種角色才可以訪問
@RestController
@RequestMapping("/test")
public class TestController {@AutowiredSysUserService sysUserService
;@GetMapping@PreAuthorize("hasRole('admin')")public Object test(){return Result.success(sysUserService
.list());}
}
此處登錄的用戶不具備該角色,所以會走權(quán)限不足過濾器
總結(jié)
以上是生活随笔為你收集整理的Security+jwt+验证码实现验证和授权的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。