从零开始,手打一个权限管理系统(第四章登录(中))
前言
这章我们来整合JWT,实现一个自定义的登录一、认证流程
我先捋一下认证的流程,方便我们后面写自定义登录
核心的类就几个,分别是:
Authentication:用户认证
AbstractAuthenticationProcessingFilter:认证处理拦截器
AuthenticationManager:处理认证
AuthenticationProvider:具体做认证的
UserDetailsService:获取用户信息
AuthenticationSuccessHandler:认证成功处理器
AuthenticationFailureHandler:认证失败处理器
我们自定义登录其实也是就是根据我们自己的需求重写这几个类。二、自定义登录
认证和授权相关的都放在base-security这个目录,方便我们后面做扩展;
自定义的这些类,其实就是仿照以UsernamePassword开头的类来写的,部分代码其实都是一样的。
1、自定义用户认证的对象JwtUserpublic 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 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 @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
代码仓库四、 体验地址
后台数据库只给了部分权限,报错属于正常!
想学的老铁给点点关注吧!!!
我是阿咕噜,一个从互联网慢慢上岸的程序员,如果喜欢我的文章,记得帮忙点个赞哟,谢谢!