zoukankan      html  css  js  c++  java
  • Security——Token认证

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

    (helloworld级别的样例代码)

    登录流程

    简单的登录流程如下:filter拦截登录请求;provider验证密码以及其它信息;验证成功走success回调,失败走failure回调。

    登录成功之后的操作:

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

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

    AuthenticationToken

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


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

    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);
        }
    }
    疯狂的妞妞 :每一天,做什么都好,不要什么都不做! 文中有不妥或者错误的地方还望指出,如果觉得本文对你有所帮助不妨【推荐】一下!如果你有更好的建议,可以给我留言讨论,共同进步!
  • 相关阅读:
    MongoDB ObjectId
    MongoDB固定集合
    MongoDB 正则表达式
    MongoDB Map Reduce
    MongoDB操作
    VIM跳到指定行
    linux之echo命令
    rpm and yum commands
    CentOS 7 下的软件安装建议
    apt系统中sources.list文件的解析
  • 原文地址:https://www.cnblogs.com/chenss15060100790/p/15761667.html
Copyright © 2011-2022 走看看