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

前言

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


四、 体验地址

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容