从零开始,手打一个权限管理系统(第四章 登录(下))

第四章 登录(下)


前言

这章我们来整合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
代码仓库


四、 体验地址

后台数据库只给了部分权限,报错属于正常!
想学的老铁给点点关注吧!!!

我是阿咕噜,一个从互联网慢慢上岸的程序员,如果喜欢我的文章,记得帮忙点个赞哟,谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值