哦哇資訊網

從零開始,手打一個許可權管理系統(第四章 登入(中))

由 阿咕嚕副業分享 發表于 美食2023-01-22

前言

這章我們來整合JWT,實現一個自定義的登入

一、認證流程

我先捋一下認證的流程,方便我們後面寫自定義登入

核心的類就幾個,分別是:

Authentication:使用者認證

AbstractAuthenticationProcessingFilter:認證處理攔截器

AuthenticationManager:處理認證

AuthenticationProvider:具體做認證的

UserDetailsService:獲取使用者資訊

AuthenticationSuccessHandler:認證成功處理器

AuthenticationFailureHandler:認證失敗處理器

我們自定義登入其實也是就是根據我們自己的需求重寫這幾個類。

二、自定義登入

認證和授權相關的都放在base-security這個目錄,方便我們後面做擴充套件;

自定義的這些類,其實就是仿照以UsernamePassword開頭的類來寫的,部分程式碼其實都是一樣的。

1、自定義使用者認證的物件JwtUser

public class JwtUser extends User {/*** 使用者ID*/@Getterprivate String id;/*** 機構ID*/@Getterprivate String orgId;public JwtUser(String id, String orgId, String username, String password, Collection<? extends GrantedAuthority> authorities) {super(username, password, authorities);this。id = id;this。orgId = orgId;}}

2、自定義JwtAuthenticationToken

程式碼其實跟UsernamePasswordAuthenticationToken差不多

public class JwtAuthenticationToken extends AbstractAuthenticationToken {/*** 登入資訊*/private final Object principal;/*** 憑證*/private final Object credentials;/*** 建立已認證的授權** @param authorities* @param principal* @param credentials*/public JwtAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {super(authorities);this。principal = principal;this。credentials = credentials;super。setAuthenticated(true);}/*** 建立未認證的授權** @param principal* @param credentials*/public JwtAuthenticationToken(Object principal, Object credentials) {//因為剛開始並沒有認證,因此使用者沒有任何許可權,並且設定沒有認證的資訊(setAuthenticated(false))super(null);this。principal = principal;this。credentials = credentials;super。setAuthenticated(false);}@Overridepublic Object getCredentials() {return this。credentials;}@Overridepublic Object getPrincipal() {return this。principal;}}

3、自定義認證攔截器JwtAuthenticationFilter

這個類也是仿照UsernamePasswordAuthenticationFilter來實現的

/*** 這個程式碼完全是仿照UsernamePasswordAuthenticationFilter來寫的* {@link UsernamePasswordAuthenticationFilter}*/public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(“/login”,“POST”);private String usernameParameter = “username”;private String passwordParameter = “password”;public JwtAuthenticationFilter() {super(DEFAULT_ANT_PATH_REQUEST_MATCHER);}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (!request。getMethod()。equals(HttpMethod。POST。name())) {throw new AuthenticationServiceException(“Authentication method not supported: ” + request。getMethod());}String username = request。getParameter(this。usernameParameter);username = (username != null) ? username。trim() : “”;String password = request。getParameter(this。passwordParameter);password = (password != null) ? password : “”;//建立未認證的tokenJwtAuthenticationToken authRequest = new JwtAuthenticationToken(username, password);//認證詳情寫入到憑著authRequest。setDetails(authenticationDetailsSource。buildDetails(request));return this。getAuthenticationManager()。authenticate(authRequest);}}

4、自定義認證處理器JwtAuthenticationProvider

大部分的程式碼也來自AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider

@Slf4j@Componentpublic class JwtAuthenticationProvider implements AuthenticationProvider {private MessageSourceAccessor messages = SpringSecurityMessageSource。getAccessor();@Getter@Setterprivate UserDetailsService userDetailsService;@Getter@Setterprivate PasswordEncoder passwordEncoder;public JwtAuthenticationProvider() {}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {UserDetails user = userDetailsService。loadUserByUsername(authentication。getName());JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;additionalAuthenticationChecks(user, jwtAuthenticationToken);//構建已認證的authenticatedTokenJwtAuthenticationToken result = new JwtAuthenticationToken(jwtAuthenticationToken。getAuthorities(), user, jwtAuthenticationToken。getCredentials());result。setDetails(authentication。getDetails());log。debug(“Authenticated user”);return result;}@Overridepublic boolean supports(Class<?> authentication) {return (JwtAuthenticationToken。class。isAssignableFrom(authentication));}/*** 直接複製的DaoAuthenticationProvider裡面的同名方法* @param userDetails* @param authentication* @throws AuthenticationException*/private void additionalAuthenticationChecks(UserDetails userDetails,JwtAuthenticationToken authentication) throws AuthenticationException {if (authentication。getCredentials() == null) {log。debug(“Failed to authenticate since no credentials provided”);throw new BadCredentialsException(this。messages。getMessage(“AbstractUserDetailsAuthenticationProvider。badCredentials”, “Bad credentials”));}String presentedPassword = authentication。getCredentials()。toString();if (!this。passwordEncoder。matches(presentedPassword, userDetails。getPassword())) {log。debug(“Failed to authenticate since password does not match stored value”);throw new BadCredentialsException(this。messages。getMessage(“AbstractUserDetailsAuthenticationProvider。badCredentials”, “Bad credentials”));}}}

5、自定義認證成功和失敗處理類

預設情況下,認證成功和失敗都是跳轉到別的頁面,我們改為返回一個json物件

5。1、認證失敗JwtAuthenticationFailureHandler

@Slf4j@Componentpublic class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {log。info(“登入失敗:{}”, exception。getLocalizedMessage());response。setContentType(MediaType。APPLICATION_JSON_VALUE);response。getWriter()。write(exception。getLocalizedMessage());response。getWriter()。flush();response。getWriter()。close();}}

5。2、認證成功JwtAuthenticationSuccessHandler

認證成功後我們需要返回一個token,所以我們需要一個Jwt的工具類JWTUtils

@Slf4j@Component@AllArgsConstructorpublic class JWTUtils {private final JwtProperties jwtProperties;public static final String ID = “id”;public static final String ORGID = “orgId”;public static final String USERNAME = “username”;public static final String AUTHORITIES = “authorities”;/*** 生成token** @param jwtUser* @return*/public String createToken(JwtUser jwtUser) {// 簽名演算法 ,將對token進行簽名SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm。HS256;byte[] apiKeySecretBytes = DatatypeConverter。parseBase64Binary(jwtProperties。getSecret());Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm。getJcaName());Map claims = Maps。newHashMap();claims。put(ID, jwtUser。getId());claims。put(ORGID, jwtUser。getOrgId());claims。put(USERNAME, jwtUser。getUsername());List list = jwtUser。getAuthorities()。stream()。collect(Collectors。toList());List stringList = list。stream()。map(GrantedAuthority::getAuthority)。collect(Collectors。toList());claims。put(AUTHORITIES, JSONUtil。toJsonStr(stringList));return Jwts。builder()。setHeaderParam(“typ”, “JWT”)。setClaims(claims)。setIssuedAt(new Date())。setExpiration(new Date(System。currentTimeMillis() + jwtProperties。getExpire() * 60 * 60 * 1000))。signWith(signatureAlgorithm, signingKey)。compact();}/*** 檢查token是否有效** @param token the token* @return the claims*/public Claims getClaimsFromToken(String token) {try {return Jwts。parser()。setSigningKey(jwtProperties。getSecret())。parseClaimsJws(token)。getBody();} catch (Exception e) {log。error(“驗證token出錯:{}”, e。getMessage());return null;}}/*** 判斷是否過期** @param claims* @return*/public boolean isTokenExpired(Claims claims) {return claims。getExpiration()。before(new Date());}/*** true 無效* false 有效** @param token* @return*/public boolean checkToken(String token) {Claims claims = getClaimsFromToken(token);if (claims != null) {return isTokenExpired(claims);}return true;}}

這裡面的jwtProperties主要用來動態配置token秘鑰和有效期,所以需要在spring。factories配置

@Slf4j@Componentpublic class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Autowiredprivate JWTUtils jwtUtils;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {//從authentication中獲取使用者資訊final JwtUser userDetail = (JwtUser) authentication。getPrincipal();log。info(“{}:登入成功”, userDetail。getUsername());response。setContentType(MediaType。APPLICATION_JSON_VALUE);String token = jwtUtils。createToken(userDetail);response。getWriter()。write(token);response。getWriter()。flush();response。getWriter()。close();}}

6、安全配置

@EnableWebSecuritypublic class SpringSecurityConfigurer {private final JwtUserDetailsService jwtUserDetailsService;private final JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandle;private final JwtAuthenticationFailureHandler jwtAuthenticationFailureHandler;public SpringSecurityConfigurer(JwtUserDetailsService jwtUserDetailsService, JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandle, JwtAuthenticationFailureHandler jwtAuthenticationFailureHandler) {this。jwtUserDetailsService = jwtUserDetailsService;this。jwtAuthenticationSuccessHandle = jwtAuthenticationSuccessHandle;this。jwtAuthenticationFailureHandler = jwtAuthenticationFailureHandler;}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http//禁用表單登入。formLogin()。disable()。authorizeRequests((authorize) -> authorize// 這裡需要將登入頁面放行,permitAll()表示不再攔截,。antMatchers(“/upms/login/**”)。permitAll()// 所有請求都要驗證。anyRequest()。authenticated())// 關閉csrf。csrf((csrf) -> csrf。disable())//禁用session,JWT校驗不需要session。sessionManagement((session) -> session。sessionCreationPolicy(SessionCreationPolicy。STATELESS));http。addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter。class);return http。build();}@BeanJwtAuthenticationFilter jwtAuthenticationFilter() {JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter();jwtAuthenticationFilter。setAuthenticationManager(authenticationManager());jwtAuthenticationFilter。setAuthenticationSuccessHandler(jwtAuthenticationSuccessHandle);jwtAuthenticationFilter。setAuthenticationFailureHandler(jwtAuthenticationFailureHandler);return jwtAuthenticationFilter;}@BeanJwtAuthenticationProvider jwtAuthenticationProvider() {JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider();//設定userDetailsServicejwtAuthenticationProvider。setUserDetailsService(jwtUserDetailsService);//設定加密演算法jwtAuthenticationProvider。setPasswordEncoder(passwordEncoder());return jwtAuthenticationProvider;}/*** 自定義的認證處理器*/@Beanpublic AuthenticationManager authenticationManager() {return new ProviderManager(jwtAuthenticationProvider());}/*** 指定加解密演算法** @return*/@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}}

三、編譯執行

經過一系列的除錯修改後,啟動專案,模擬登入請求,看到如下介面就表示成功了。

當前版本tag:1。0。3

程式碼倉庫

四、 體驗地址

後臺資料庫只給了部分許可權,報錯屬於正常!

想學的老鐵給點點關注吧!!!

我是阿咕嚕,一個從網際網路慢慢上岸的程式設計師,如果喜歡我的文章,記得幫忙點個贊喲,謝謝!

TAG: returnstring認證publictoken