Security——Token认证

主要就讲解一下,如何使用Security实现token认证,顺带也讲讲默认的登录,是如何实现的。

主要目的是:了解Security在用户认证方面,给我们提供了哪些接口,都是做什么的。

登录流程

简单的登录流程如下:

​ filter拦截登录请求;provider验证密码以及其它信息;验证成功走success回调,失败走failure回调。

登录成功之后的操作:

1、如果是token认证,成功之后需要写回token,之后客户端的每一个请求,都需要携带token,此外,还需要一个独立的filter,拦截所有的请求,判断token是不是有效的。

2、如果是session,那就往session中存储用户信息。

img

img

AuthenticationToken

使用Security登录时,需要将用户信息封装成Authentication,Authentication包含了登录所需的关键参数,整个认证流程都会有Authentication的参与。

1、AuthenticationToken是Authentication的子类,刚开始学习时不要因为名称,就把它们看做是两个不同的对象;
2、这个对象包含了用户的账号、密码,以及其它登录所需的的信息;
3、这个对象是有状态变化的,“未认证的” 和 “已经完成认证的”(这里实际上说的是setAuthenticated(false)函数);
4、设计Authentication类的时候,除非设计需要,否则尽量避免采用继承的写法,避免Token被其它Provider解析(不要继承其它实现类,不是指Security提供的接口)。

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * 直接复制{@link UsernamePasswordAuthenticationToken}全部源码,根据自己的需求进行代码扩展;
 * 注意{@link UsernamePasswordAuthenticationToken}的构造函数,2个参数的和3个参数的,效果是不一样的;
 * 因为每一个Token都有对应的Provider,最好避免采用继承的方式写Token。
 *
 * @author Mr.css
 * @date 2021-12-23 10:51
 */
public class AuthenticationToken extends AbstractAuthenticationToken {
	//这里省略全部代码
}

AbstractAuthenticationProcessingFilter

功能类似于Servlet的Filter,代码执行的优先级非常高,即使没有配置Sevlet或者Controller,代码也可以执行。
代码执行最终,需要返回用户的认证信息(已经认证完毕),如果认证成功,继续走成功的回调接口,如果认证失败,就走失败的接口。

1、想看Security默认的功能实现,可以参考UsernamePasswordAuthenticationFilter代码;
2、主要功能,就是拦截登录请求,发起用户认证,最终返回已经完成认证的Authentication;
3、代码不会自动调用Provider,需要手动执行super.getAuthenticationManager().authenticate(authentication)函数。

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登录验证
 *
 * @author Mr.css
 * @date 2021-12-23 11:41
 */
public class LoginFilter extends AbstractAuthenticationProcessingFilter {

    LoginFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    /**
     * 尝试认证,获取request中的数据,发起认证
     *
     * @param request  -
     * @param response -
     * @return returning a fully populated Authentication object (including granted authorities)
     * @throws AuthenticationException -
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String userName = request.getParameter("userName");
        String pwd = request.getParameter("pwd");
        System.out.println("结果过滤器拦截...");
        AuthenticationToken authentication = new AuthenticationToken(userName, pwd);

        //发起认证,经过程序流转,最终会到达Provider
        return super.getAuthenticationManager().authenticate(authentication);
    }
}

AuthenticationProvider

Provider包含两个主要功能,一个是查询,一个就是认证,找到用户的详细信息,然后证明用户的账号、密码都是有效的。

这个类包含两个函数:
supports(Class<?> authentication)用于说明当前的Provider可以解析哪些Authentication,
authenticate(Authentication authentication)认证用户信息,参数与返回值类型完全一致,完成认证之后,可以将参数直接返回。

import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

/**
 * 用户认证
 *
 * @author Mr.css
 * @date 2021-12-23 10:53
 */
public class AuthenticationProvider extends DaoAuthenticationProvider {

    /**
     * 标明当前Provider能够处理的Token类型
     *
     * @param authentication tokenClass
     * @return boolean
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return AuthenticationToken.class == authentication;
    }

    /**
     * 身份鉴权
     *
     * @param authentication 身份证明
     * @return Authentication  已经完成的身份证明(a fully authenticated object including credentials)
     * @throws AuthenticationException e
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        System.out.println("身份认证:" + authentication);
        return authentication;
    }
}

UserDetailsService

相当于DAO,主要就是负责用户身份信息查询,包括密码、权限,下面代码是生产环境直接扣出来的,提供参考代码,按需调整。

import cn.seaboot.admin.user.bean.entity.Role;
import cn.seaboot.admin.user.bean.entity.User;
import cn.seaboot.admin.user.bean.entity.UserGroup;
import cn.seaboot.admin.user.service.PermService;
import cn.seaboot.admin.user.service.RoleService;
import cn.seaboot.admin.user.service.UserGroupService;
import cn.seaboot.admin.user.service.UserService;
import cn.seaboot.common.core.CommonUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * 查询用户详细信息
 *
 * @author Mr.css
 * @date 2020-05-08 0:02
 */
@Configuration
public class CustomUserDetailsService implements UserDetailsService {

    @Resource
    private PasswordHelper passWordHelper;

    @Resource
    private UserService userService;

    @Resource
    private RoleService roleService;

    @Resource
    private UserGroupService userGroupService;

    @Resource
    private PermService permService;

    /**
     * 因为security自身的设计原因,角色权限前面需要添加ROLE前缀
     */
    private static final String ROLE_PREFIX = "ROLE_";
    /**
     * 默认添加一个权限,名称为登录,标明必须登录才能访问(个性化设计:只是为了方便组织代码逻辑)
     */
    private static final String ROLE_LOGIN = "ROLE_LOGIN";

    /**
     * 因为security自身的设计原因,我们在用户分组和角色权限,增加ROLE前缀
     *
     * @param role 角色
     * @return SimpleGrantedAuthority
     */
    private SimpleGrantedAuthority genSimpleGrantedAuthority(String role) {
        if (!role.startsWith(ROLE_PREFIX)) {
            role = ROLE_PREFIX + role;
        }
        return new SimpleGrantedAuthority(role);
    }

    /**
     * 用户登录并赋予权限
     *
     * @param userName 用户帐号
     * @return UserDetails 用户详细信息
     * @throws UsernameNotFoundException 抛出具体的异常
     */
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        System.out.println("登录的用户是:" + userName);
        User user = userService.queryByUserCode(userName);
        UserGroup userGroup = userGroupService.queryById(user.getOrgId(), user.getGroupId());
        Role sysRole = roleService.queryById(user.getOrgId(), userGroup.getRoleId());
        //用户权限列表
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        //添加用户组权限
        if (CommonUtils.isNotEmpty(userGroup.getCode())) {
            String role = userGroup.getCode();
            grantedAuthorities.add(this.genSimpleGrantedAuthority(role));
        }
        //添加角色权限
        if (CommonUtils.isNotEmpty(sysRole.getRoleCode())) {
            String role = sysRole.getRoleCode();
            grantedAuthorities.add(this.genSimpleGrantedAuthority(role));
        }
        //添加普通权限
        Set<String> perms = permService.selectConcisePermsByRoleId(userGroup.getRoleId());
        for (String perm : perms) {
            if (CommonUtils.isNotEmpty(perm)) {
                grantedAuthorities.add(new SimpleGrantedAuthority(perm));
            }
        }
		
		// TODO: 测试时可以删除上面其它权限配置,这里仅提供参考
        grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_LOGIN));
        // TODO:获取BCrypt加密的密码,按需调整,这里我用的是自己的加密算法,可以直接使用BCryptPasswordEncoder
        String bCryptPassword = passWordHelper.getBCryptPassword(user.getPassword(), user.getPasswordSalt());
        return new org.springframework.security.core.userdetails.User(user.getUserCode(), bCryptPassword, grantedAuthorities);
    }
}

AuthenticationSuccessHandler

身份认证成功回调函数,如果普通登录,就进行页面转发,如果是token认证,就向客户端写回一个token。

import cn.seaboot.admin.mvc.Result;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 身份认证成功
 *
 * @author Mr.css
 * @date 2021-12-23 11:59
 */
public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {


    /**
     * Called when a user has been successfully authenticated.
     * 认证成功之后调用
     *
     * @param request        -
     * @param response       -
     * @param authentication 认证信息
     * @throws IOException -from write
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        System.out.println("身份认证成功:" + authentication);
        //TODO: 登录成功,将token写回客户端
        response.getWriter().write(JSON.toJSONString(Result.succeed()));
    }
}

AuthenticationFailureHandler

在认证过程中,出现认证问题,需要抛出异常,在这里统一处理。

import cn.seaboot.admin.mvc.Result;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登录失败异常处理
 *
 * @author Mr.css
 * @date 2021-12-23 12:01
 */
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Result result;
        if (exception instanceof BadCredentialsException ||
                exception instanceof UsernameNotFoundException) {
            result = Result.failed("账户名或者密码输入错误!");
        } else if (exception instanceof LockedException) {
            result = Result.failed("账户被锁定,请联系管理员!");
        } else if (exception instanceof CredentialsExpiredException) {
            result = Result.failed("密码过期,请联系管理员!");
        } else if (exception instanceof DisabledException) {
            result = Result.failed("账户被禁用,请联系管理员!");
        } else {
            result = Result.failed("登陆失败!");
        }
        response.getWriter().write(result.toString());
    }
}

TokenFilter

不管是session,还是使用token,登录成功之后,都需要一个独立的filter,拦截所有的请求,证明你已经登陆过了。

如果是token认证,就需要验证,你的每一个请求是否包含了token,并且需要验证token是否还有效。

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 每一次请求都需要校验一次token
 *
 * @author Mr.css
 * @date 2021-12-23 15:14
 */
public class TokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String token = httpServletRequest.getHeader("Authentication");
        System.out.println("token" + token);

        //授权
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
		//grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_LOGIN"));

        //正常设计,在LoginFilter那一步就必须创建Authentication,这里为了演示,创建一个虚拟的Authentication。
        AuthenticationToken authenticationToken = new AuthenticationToken("admin", "test", grantedAuthorities);
        authenticationToken.setDetails(new WebAuthenticationDetails(httpServletRequest));
		//添加到上下文中
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

		//未登录直接抛出异常,交给spring异常切面统一处理,也可以自定义其它处理方式
        //throw new AccessDeniedException("登录未授权!");
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

配置类

需要注意黑名单和白名单的配置,避免直接拒绝接收登录接口。

import cn.seaboot.admin.security.bean.entity.SecurityChain;
import cn.seaboot.common.core.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;
import java.util.List;

/**
 * Security configuration
 *
 * @author Mr.css
 * @date 2020-05-07 23:38
 */
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    private Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);

    @Resource
    private CustomUserDetailsService customUserDetailsService;

    @Resource
    private BCryptPasswordEncoder passwordHelper;

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /**
     * HttpSecurity相关配置
     *
     * @param http HttpSecurity
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
		
        //前面我们设置的登陆接口是/login,因此/login的配置是permitAll
        registry.antMatchers("/login").access("permitAll");

        // 添加拦截器
        LoginFilter loginFilter = new LoginFilter();
        TokenFilter tokenFilter = new TokenFilter();

        loginFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
        loginFilter.setAuthenticationFailureHandler(new LoginFailureHandler());

        loginFilter.setAuthenticationManager(super.authenticationManager());
        http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAfter(tokenFilter, LoginFilter.class);

        //禁用CSRF,默认用于防止CSRF攻击的设置,模版引擎中使用
        http.csrf().disable();

        // 基于token,所以不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //禁用掉XFrameOptions,这个配置能让IFrame无法嵌套我们的页面,可以防止盗链,
        http.headers().frameOptions().disable();
    }

    /**
     * 设置用户登录和密码加密功能
     *
     * @param auth AuthenticationManagerBuilder
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        AuthenticationProvider authenticationProvider = new AuthenticationProvider();
        authenticationProvider.setUserDetailsService(customUserDetailsService);
        authenticationProvider.setPasswordEncoder(passwordHelper);
        auth.authenticationProvider(authenticationProvider);
    }
}
posted @ 2022-10-28 22:13  Little_Monster-lhq  阅读(1386)  评论(0编辑  收藏  举报