第四章 登录(下)
文章目录
前言
这章我们来整合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
代码仓库
四、 体验地址
后台数据库只给了部分权限,报错属于正常!
想学的老铁给点点关注吧!!!
我是阿咕噜,一个从互联网慢慢上岸的程序员,如果喜欢我的文章,记得帮忙点个赞哟,谢谢!