spring boot +spring security + jwt 实现认证模块

1,609 阅读6分钟

        我在使用spring进行开发时,通常是使用 aop+jwt 模式来对调用者身份进行确认。前几天接触到一个开源商城源码(github地址)里面使用spring security +jwt 来进行权限的验证。但是源码中只实现了简单的用户名密码验证,关于权限的略过了。虽然以前了解过spring security但是没有实际使用过,借着这个机会整合了一下spring security jwt。(可以拿起就用:smirk:) github源码地址

        spring security 是基于spring的一个web安全框架。一般来说,web应用的安全性包括用户认证和用户授权两个部分。用户认证常见的就是用户名密码验证。用户授权则指的是查看用户是否有权限调用资源。

用户认证

对于用户认证,我们自定义的话通常需要自己实现 PasswordEncoder UserDetail两个类。PasswordEncoder主要实现了密码的加密,以及密码的比较(登陆时用户密码与数据库存储的密码)。我实现的PasswordEncoder代码如下

package com.lichaobao.springsecurityjwt.component;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author lichaobao
 * @date 2018/12/22
 * @QQ 1527563274
 */
public class MyPasswordEncoder implements PasswordEncoder {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyPasswordEncoder.class);

    /**
     * 自定义密码加密(出于示例,本代码没有对密码进行加密,直接返回原密码)
     * @param charSequence 需要加密的密码
     * @return 加密后的密码
     */
    @Override
    public String encode(CharSequence charSequence) {
        LOGGER.info("now encode password :{}",charSequence.toString());
        return charSequence.toString();
    }

    /**
     * 比较加密后的密码与数据库中的密码是否匹配
     * @param charSequence 用户登陆传来的密码
     * @param s 数据库中存储的密码
     * @return true 匹配 false 不匹配
     */
    @Override
    public boolean matches(CharSequence charSequence, String s) {
        LOGGER.info("matchs charSequence :{} and password :{}",charSequence,s);
        return encode(charSequence).equals(s);
    }
}

        对于UserDetails类来说主要起到了封装用户信息的作用,包括用户的基本信息以及拥有的权限信息的封装。默认UserDetails的生成类是 UserDetailsService。 这个接口中提供了loadUserByUsername(String username)方法UserDetailService源码如下

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

在本例中 我们通过修改WebSecurityConfigAdapter中的UserdetailsServices实现代码如下

    /**
     * 在次代码中完成用户基本信息的查询比如用户名 密码 权限等封装后 返回
     * 此方法的入口 为 userDetailsService.loadUserByUsername(String username)
     * @return UserDetail
     */
    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        return username ->{
            if(users.containsKey(username)){
                return new MyUserDetails(username,users.get(username),permissions.get(username));
            }
            throw new UsernameNotFoundException("用户名错误");
        };
    }

权限验证

        自定义权限验证我们通过自定义AccesDecisionVoter类来实现。关键代码如下

/**
 * @author lichaobao
 * @date 2018/12/22
 * @QQ 1527563274
 */
public class RoleBasedVotor implements AccessDecisionVoter {
    private static final Logger LOGGER = LoggerFactory.getLogger(RoleBasedVotor.class);
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;//根据自己的逻辑修改  不能直接return false否则验证不通过
    }

    /**
     * 主要验证逻辑
     * ROLE_ANONYMOUS 代表所有人可以访问 这是 spring security 自动生成的 可以自定义
     * @param authentication 用户信息
     * @param o  可以从这里拿到url
     * @param collection 访问资源需要的权限在本例中由于我们将url作为验证依据所以为用到collection
     * @return ACCESS_DENIED(-1)无权限 ACCESS_GRANTED(1)有权限
     */
    @Override
    public int vote(Authentication authentication, Object o, Collection collection) {
        FilterInvocation fi = (FilterInvocation) o;
        String url = fi.getRequestUrl();
        LOGGER.info("url :{}",url);
        if(authentication == null){
            return ACCESS_DENIED;
        }
        Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
        Iterator iterator = authorities.iterator();
        while (iterator.hasNext()){
            GrantedAuthority ga = (GrantedAuthority) iterator.next();
            LOGGER.info(ga.getAuthority());
            if(equalsurl(url,ga.getAuthority())||"ROLE_ANONYMOUS".equals(ga.getAuthority())){
                return ACCESS_GRANTED;
            }
        }
        return ACCESS_DENIED;
    }

    @Override
    public boolean supports(Class aClass) {
        return true;//根据自己的逻辑修改  不能直接return false否则验证不通过
    }

    /**
     * 获得用户权限信息
     * @param authentication
     * @return
     */
    Collection<? extends GrantedAuthority> extractAuthorities(
            Authentication authentication) {
        LOGGER.info("extractAuthorites:{}",authentication.getAuthorities());
        return authentication.getAuthorities();
    }

    /**
     * 比较权限 权限 /** 代表 以下所有能访问 /* 代表以下一级能访问 如 用户权限为 /test/** 则能访问 /test/a /test/b/c
     * 如用户权限为 /test/* 则能访问 /test/a 而 /test/b/c则不能访问
     * @param url 访问的url
     * @param urlpermission 拥有的权限
     * @return boolean
     */
     static boolean equalsurl(String url,String urlpermission) {
        url = url.startsWith("/") ? url.substring(1):url;
        urlpermission = urlpermission.startsWith("/")?urlpermission.substring(1):urlpermission;
        if("**".equals(urlpermission)){
            return true;
        }else if("*".equals(urlpermission)){
            return url.split("/").length == 1;
        }
        else if(urlpermission.endsWith("/**")){
            String afterUrl =  urlpermission.substring(0,urlpermission.length()-3);
            return url.startsWith(afterUrl);
        }else if(urlpermission.endsWith("/*")){
            String afterUrl = urlpermission.substring(0,urlpermission.length()-2);
            String[] urlPiece = url.split("/");
            return url.startsWith(afterUrl)&&urlPiece.length == 2;
        }
        return url.equals(urlpermission);
    }
}

验证登陆

        在登陆逻辑的代码中,我们需要通过登陆接口接收到的用户名、密码生成UsernamePasswordAuthenticationToken 为接下来的验证提供一个桥梁。注意密码要根据自定义实现的PasswordEncoder中的加密方法或着其他自己实现的加密方法进行加密要保证加密后和数据库中的密码相对应。然后 通过调用authenticationManager中的authenticate方法进行验证。具体代码如下

@Service
public class SignServiceImpl implements SignService {
    private static final Logger LOGGER = LoggerFactory.getLogger(SignService.class);
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    UserDetailsService userDetailsService;
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    PasswordEncoder passwordEncoder;

    /**
     * 登陆 登陆出现错误抛出错误 用catch接受即可
     * @param username 用户名
     * @param password 密码
     * @return String token
     */
    @Override
    public String login(String username, String password) {
        String token = null;
        /**
         * 封装 注意密码加密
         */
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username,passwordEncoder.encode(password));
        try{
            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            /**
             * 加载数据库中的用户名密码 主要逻辑为UserdetailsServices中的代码
             */
            userDetailsService.loadUserByUsername(username);
            token = jwtUtils.generateToken(username);
        }catch (Exception e){
            e.printStackTrace();
            LOGGER.info("认证失败 :{}",e.getMessage());
        }
        return token;
    }
}

验证登陆凭证

        具体实现为继承OncePerRequestFilter方法实现自己的Filter ,通过解析token获得用户信息,然后比对用户权限。代码如下:

/**
 * @author lichaobao
 * @date 2018/12/22
 * @QQ 1527563274
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if(token != null && SecurityContextHolder.getContext().getAuthentication() == null){
            String username = jwtUtils.getUserNameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
            LOGGER.info("UserDetails :{},permissions:{}",userDetails.getUsername(),userDetails.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            LOGGER.info("authenticated user :{}",username);
            LOGGER.info("already filter name:{}",super.getAlreadyFilteredAttributeName());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request,response);
    }
}

配置security

        具体实现为继承WebSecurityConfigurerAdapter方法 根据自己的需要重写逻辑

/**
 * @author lichaobao
 * @date 2018/12/22
 * @QQ 1527563274
 */
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    MyAccessDeineHandler myAccessDeineHandler;

    @Autowired
    MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    /**
     * 模拟数据库用户
     */
    private static Map<String,String> users;
    /**
     * 模拟权限
     */
    private static Map<String,List<String>> permissions;
    static {
        users = new HashMap<>();
        permissions = new HashMap<>();
        users.put("a","a");
        String[] aper = new String[]{"/a/**","/test/all"};
        permissions.put("a",Arrays.asList(aper));
        users.put("b","b");
        String[] bper = new String[]{"/b/**","test/all"};
        permissions.put("b",Arrays.asList(bper));
        users.put("admin","password");
        String[] adminPer = new String[]{"/**"};
        permissions.put("admin",Arrays.asList(adminPer));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http.csrf()
                    .disable()//禁用csrf 因为使用jwt不需要
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)//禁用session
                    .and()
                    .authorizeRequests()
                    .accessDecisionManager(accessDecisionManager())//加载自己的accessDecisionManager 用到了RoleBasedVotor
                    .antMatchers(HttpMethod.GET,
                            "/",
                            "/*.html",
                            "/favicon.ico",
                            "/**/*.html",
                            "/**/*.css",
                            "/**/*.js",
                            "/swagger-resources/**",
                            "/v2/api-docs/**")
                    .permitAll()//允许访问所有界面资源
                    .antMatchers("/login","/register")
                    .permitAll()//允许访问登陆注册接口  然后  "/login"与"/register"的权限为"ROLE_ANONYMOUS"
                    .antMatchers(HttpMethod.OPTIONS)
                    .permitAll()//跨域请求 会有一个OPTIONS 请求  全部允许
                    .anyRequest()//其他任何都需要验证
                    .authenticated();
        /**
         * 禁用缓存
         */
        http.headers().cacheControl();
        /**
         * 配置自定义的Filter
         */
            http.addFilterBefore(jwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
        /**
         * 配置自定义的无权限以及用户名密码错误返回结果
         */
        http.exceptionHandling()
                    .accessDeniedHandler(myAccessDeineHandler)
                    .authenticationEntryPoint(myAuthenticationEntryPoint);
    }

    /**
     * 配置 userDetailsService 以及passwordEncoder;
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    /**
     * 在次代码中完成用户基本信息的查询比如用户名 密码 权限等封装后 返回
     * 此方法的入口 为 userDetailsService.loadUserByUsername(String username)
     * @return UserDetail
     */
    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        return username ->{
            if(users.containsKey(username)){
                return new MyUserDetails(username,users.get(username),permissions.get(username));
            }
            throw new UsernameNotFoundException("用户名错误");
        };
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new MyPasswordEncoder();
    }
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
        return new JwtAuthenticationTokenFilter();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 具体使用RoleBasedVotor方法
     * @return
     */
    @Bean
    public AccessDecisionManager accessDecisionManager(){
        List<AccessDecisionVoter<? extends Object>> decisionVoters
                = Arrays.asList(
                new WebExpressionVoter(),
                new RoleBasedVotor(),
                new AuthenticatedVoter());
        return new UnanimousBased(decisionVoters);
    }
}