天天看點

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

作者:阿咕噜副業分享

前言

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

一、認證流程

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

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

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

Authentication:使用者認證

AbstractAuthenticationProcessingFilter:認證處理攔截器

AuthenticationManager:處理認證

AuthenticationProvider:具體做認證的

UserDetailsService:擷取使用者資訊

AuthenticationSuccessHandler:認證成功處理器

AuthenticationFailureHandler:認證失敗處理器

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

二、自定義登入

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

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

1、自定義使用者認證的對象JwtUser

public class JwtUser extends User {
/**
* 使用者ID
*/
@Getter
private String id;
/**
* 機構ID
*/
@Getter
private 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);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public 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);
}
@Override
public 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 : "";
//建立未認證的token
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(username, password);
//認證詳情寫入到憑着
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(authRequest);
}
}
           

4、自定義認證處理器JwtAuthenticationProvider

大部分的代碼也來自AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider

@Slf4j
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
@Getter
@Setter
private UserDetailsService userDetailsService;
@Getter
@Setter
private PasswordEncoder passwordEncoder;
public JwtAuthenticationProvider() {
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UserDetails user = userDetailsService.loadUserByUsername(authentication.getName());
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
additionalAuthenticationChecks(user, jwtAuthenticationToken);
//建構已認證的authenticatedToken
JwtAuthenticationToken result = new JwtAuthenticationToken(jwtAuthenticationToken.getAuthorities(), user, jwtAuthenticationToken.getCredentials());
result.setDetails(authentication.getDetails());
log.debug("Authenticated user");
return result;
}
@Override
public 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
@Component
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public 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
@AllArgsConstructor
public 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<String, Object> claims = Maps.newHashMap();
claims.put(ID, jwtUser.getId());
claims.put(ORGID, jwtUser.getOrgId());
claims.put(USERNAME, jwtUser.getUsername());
List<GrantedAuthority> list = jwtUser.getAuthorities().stream().collect(Collectors.toList());
List<String> 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
@Component
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JWTUtils jwtUtils;
@Override
public 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、安全配置

@EnableWebSecurity
public 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;
}
@Bean
public 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();
}
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter();
jwtAuthenticationFilter.setAuthenticationManager(authenticationManager());
jwtAuthenticationFilter.setAuthenticationSuccessHandler(jwtAuthenticationSuccessHandle);
jwtAuthenticationFilter.setAuthenticationFailureHandler(jwtAuthenticationFailureHandler);
return jwtAuthenticationFilter;
}
@Bean
JwtAuthenticationProvider jwtAuthenticationProvider() {
JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider();
//設定userDetailsService
jwtAuthenticationProvider.setUserDetailsService(jwtUserDetailsService);
//設定加密算法
jwtAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return jwtAuthenticationProvider;
}
/**
* 自定義的認證處理器
*/
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(jwtAuthenticationProvider());
}
/**
* 指定加解密算法
*
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
           

三、編譯運作

經過一系列的調試修改後,啟動項目,模拟登入請求,看到如下界面就表示成功了。

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

目前版本tag:1.0.3

代碼倉庫

四、 體驗位址

背景資料庫隻給了部分權限,報錯屬于正常!

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

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

繼續閱讀