Security + JWT 实现基于Token自定义登录参数验证

Security + JWT 实现基于Token自定义登录参数验证



简介

通俗易懂的说就是用户名+密码进行登录,后端服务器验证用户名和密码是否正确,获取用户权限等,然后生成token返回,下次调用接口时在请求头添加token,后端通过解析token来完成用户登录和权限验证


框架核心组件

1. AuthenticationManager:用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做(文章中自定义登录采用此处的实现类来具体实现)。

2. AuthenticationProvider,:认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider。

3. UserDetailService:获取用户信息的接口spring-security抽象成UserDetailService,获取用户具体信息,也可以通过这个servcieImpl实现类具体实现。

4. AuthenticationToken,:所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现。

5. SecurityContext:当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。

二、使用步骤

1.pom

<!-- spring security 安全认证 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.自定义实现类

由于系统中包含了用户名+密码/手机号+验证码登录方式,所以我们需要自己实现登录验证。此篇文章将通过AuthenticationManager用户认证管理类来实现。

  1. SecurityConfig配置
	 /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return new MyAuthenticationManager();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/captchaImage").anonymous()

                .antMatchers("/**/**/login").anonymous()
                .antMatchers("/**/**/register").anonymous()
                .antMatchers("/**/**/smsCode").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).permitAll()
                .antMatchers("/profile/**").anonymous()
                .antMatchers("/common/download**").anonymous()
                .antMatchers("/common/download/resource**").anonymous()
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                .antMatchers("/druid/**").anonymous()
                // Spring Boot Admin Server 的安全配置
                .antMatchers(adminServerProperties.getContextPath()).anonymous()
                .antMatchers(adminServerProperties.getContextPath() + "/**").anonymous()
                // Spring Boot Actuator 的安全配置
                .antMatchers("/actuator").anonymous()
                .antMatchers("/actuator/**").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    
    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

此处返回MyAuthenticationManager为我们自定义的认证类。配置认证成功和失败等接口。
通过配置authenticationManagerBean这个方法,下边configure身份认证接口将不起作用。

  1. MyAuthenticationManager 自定义认证类
public class MyAuthenticationManager implements AuthenticationManager, AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private DoctorDetailsService doctorDetailsService;
    @Autowired
    private MemberDetailsService memberDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private SysPermissionService permissionService;
    @Autowired
    private IWbDoctorService wbDoctorService;

    private final String webType = "web";
    private final String doctorType = "doctor";
    private final String memberType = "member";
    private static final Logger log = LoggerFactory.getLogger(MyAuthenticationManager.class);

    public MyAuthenticationManager() {
        System.out.println("自定义用戶验证");
    }

    /**
     * @description 通过查看结构树可以看到,我们可以使用UsernamePasswordAuthenticationToken这个来实现用户的登录控制
     *              UsernamePasswordAuthenticationToken 用户名密码的令牌
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        return myAuthenticate(authentication);
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return aClass.equals(UsernamePasswordAuthenticationToken.class);
    }

    public Authentication myAuthenticate(Authentication authentication) throws AuthenticationException {
        // 简化上面的操作
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        // 或者 token.getPrincipal();获取用户名
        String loginName = token.getName();
		//LoginData为我们自定义的类,通过UsernamePasswordAuthenticationToken调用token.setDetails(data);进去的,除了用户名和密码,其它手机号验证码登录类型等都放在此处。
        LoginData data  = (LoginData) token.getDetails();
		//userDetailsService框架中存在的接口,我们写个实现类,让他执行即可,可以获取到登录信息
		UserDetails userDetails = userDetailsService.loadUserByUsername(loginName);
		String presentedPassword = authentication.getCredentials().toString();
		//框架底层密码验证也是这么匹配的,如果不匹配直接抛出一个密码错误的异常
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                log.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException("AbstractUserDetailsAuthenticationProvider.badCredentials");
            }
        return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword());
    }
}
  1. UserDetailsService接口
    在这里插入图片描述
    代码中显示为jar中存在的接口,所以我们去写个实现类就可以

  2. UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        SysUser user = userService.selectUserByUserName(username);
        if (Validator.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user)
    {
        return new LoginUser(user, permissionService.getMenuPermission(user));
    }

写到这儿我们可以看见返回了一个LoginUser,这个类中包含的SysUser为刚刚调用数据查询出来的,此处做了一个账号状态校验,也可以添加其它校验逻辑。
然后回到我们前边自定义认证管理类中,可以校验登录密码是否正确,或者通过UsernamePasswordAuthenticationToken中

LoginData data  = (LoginData) token.getDetails();

可以获取到手机号密码进行账号验证,只需要加上短信验证机制就可以,因为账号状态等我们已经校验完成了,当用此方式时,返回代码如下:

return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword());

userDetails中如果是自定义的用户类,那么SysUser对象需要传入userId,userName,password,status最基础字段。因为登录为两种情况,我们没有做过多的改动,用了同一个类来生成token,如果需要区分的话,可以在生成token时根据类型去获取不同的用户类来生成。
到此处登录基本上算是完成了,因为没有写过多的自定义类。
最后再看一眼登录实现。
5. loginService

 Authentication authentication = null;
        try
        {
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
            LoginData data = new LoginData();
            data.setCode(code);
            data.setUuid(uuid);
            data.setUserType(userType);
            data.setIsFlag(isFlag);
            token.setDetails(data);
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(token);
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new CustomException(e.getMessage());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenService.createToken(loginUser);

登录生成token是用LoginUser 上边已经说过了,不同类型可以单独区分生成token。
基本算是比较详细了,下面链接只包含登录认证部分,不包含其它
部分代码

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值