单点登录——JWT+Security(token认证)

大家好~昨天一直有个疑问,微服务和多模块springboot项目是怎么拦截所有请求,去解析token的(不可能每个服务都写个拦截器或者过滤器),然后自己百度了资料,按自己的想法测试了下,发现是可以的,今天写篇文章记录下。

流程:
1.准备一个springboot项目
2.引入包
3.配置类
4.测试接口

1.之前我用的是单模块的springboot项目,现在换成多模块的springboot项目
1.1新建两个maven项目 test1 test2 (test1为资源服务项目 test2 为公用服务)在父项目single_sign_on引入springboot依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>
    <!--字符集-->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
image.png

1.2 test2服务导入需要的依赖

    <dependencies>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--security权限依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
        </dependency>
        <!--lombok依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

1.3 在test2里面创建token的校验配置类+SpringSecurityConfig配置类

package cn.huangsong.config;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

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.*;

/**
 * token的校验
 * 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
 * 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
 * 如果校验通过,就认为这是一个取得授权的合法请求
 * @author xxm
 */

public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String url = request.getRequestURI();
        String header = request.getHeader(JwtUtil.AUTHORIZATION);
        response.setCharacterEncoding("UTF-8");
        Map<String,Object> json=new HashMap<>();
        //跳过不需要验证的路径
        if(null != SpringSecurityConfig.AUTH_WHITELIST&&Arrays.asList(SpringSecurityConfig.AUTH_WHITELIST).contains(url)){
            //chain.doFilter将请求转发给过滤器链下一个filter , 如果没有filter那就是你请求的资源
            chain.doFilter(request, response);
            return;
        }
        if (StringUtils.isEmpty(header) || !header.startsWith(JwtUtil.TOKEN_PREFIX)) {
            json.put("codeCheck", false);
            json.put("msg", "Token为空");
            response.getWriter().write(json.toString());
            return;
        }
        try {
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request,response);
            //封装好的用户信息交给Security管理
            SecurityContextHolder.getContext().setAuthentication(authentication);
            //chain.doFilter将请求转发给过滤器链下一个filter , 如果没有filter那就是你请求的资源
            chain.doFilter(request, response);
        }catch (ExpiredJwtException e) {
            json.put("codeCheck", false);
            json.put("msg", "Token已过期");
            response.getWriter().write(json.toString());
            logger.error("Token已过期: {} " + e);
        } catch (UnsupportedJwtException e) {
            json.put("codeCheck", false);
            json.put("msg", "Token格式错误");
            response.getWriter().write(json.toString());
            logger.error("Token格式错误: {} " + e);
        } catch (MalformedJwtException e) {
            json.put("codeCheck", false);
            json.put("msg", "Token没有被正确构造");
            response.getWriter().write(json.toString());
            logger.error("Token没有被正确构造: {} " + e);
        } catch (SignatureException e) {
            json.put("codeCheck", false);
            json.put("msg", "Token签名失败");
            response.getWriter().write(json.toString());
            logger.error("签名失败: {} " + e);
        } catch (IllegalArgumentException e) {
            json.put("codeCheck", false);
            json.put("msg", "Token非法参数异常");
            response.getWriter().write(json.toString());
            logger.error("非法参数异常: {} " + e);
        }catch (Exception e){
            json.put("codeCheck", false);
            json.put("msg", "Invalid Token");
            response.getWriter().write(json.toString());
            logger.error("Invalid Token " + e.getMessage());
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request,HttpServletResponse response)  {
        String token = request.getHeader(JwtUtil.AUTHORIZATION);
        if (token != null) {
            // 解密Token
            Map map = JwtUtil.validateToken(token);
            //获取用户名
            String userName = map.get("userName").toString();
            //获取权限
            List<Map<String,String>> authorities = (List)map.get("authorities");
            List<GrantedAuthority> permissions = new ArrayList<>();
            if(authorities.size()>0){
                for (Map<String, String> authority : authorities) {
                    permissions.add(new SimpleGrantedAuthority(authority.get("authority")));
                }
            }
            if (!StringUtils.isEmpty(userName)) {
                //把用户名和权限封装成UsernamePasswordAuthenticationToken交给Security管理,第二个参数是密码
                return new UsernamePasswordAuthenticationToken(userName, null, permissions);
            }
        }
        return null;
    }
}

JwtUtil工具类

package cn.huangsong.config;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;


public class JwtUtil {
    /**过期时间---1小时*/
    private static final int EXPIRATION_TIME = 60*60;
    /**自己设定的秘钥*/
    //private static final String SECRET = "023bdc63c3c5a4587*9ee6581508b9d03ad39a74fc0c9a9cce604743367c9646b";
    private static final String SECRET = "123456";
    /**前缀*/
    public static final String TOKEN_PREFIX = "Bearer ";
    /**表头授权*/
    public static final String AUTHORIZATION = "Authorization";

    /**
     *
     * 功能描述:创建Token
     * @date: 2020/5/28 16:09
     * @param: 
     * @return: 
     */
    public static String generateToken(UserDetails userDetails) {
        Calendar calendar = Calendar.getInstance();
        Date now = calendar.getTime();
        // 设置签发时间
        calendar.setTime(new Date());
        // 设置过期时间
        // 添加秒钟
        calendar.add(Calendar.SECOND, EXPIRATION_TIME);
        Date time = calendar.getTime();
        HashMap<String, Object> map = new HashMap<>();
        map.put("userName", userDetails.getUsername());
        map.put("authorities",userDetails.getAuthorities());
        String jwt = Jwts.builder()
                .setClaims(map)
                //签发时间
                .setIssuedAt(now)
                //过期时间
                .setExpiration(time)
                .signWith(SignatureAlgorithm.HS256, SECRET)
                .compact();
        //jwt前面一般都会加Bearer
        return TOKEN_PREFIX + jwt;
    }
    /**
     *
     * 功能描述: 解密Token
     * @date: 2020/5/28 16:18
     * @param: 
     * @return: 
     */
    public static Map validateToken(String token) {
        Map<String, Object> body = Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                .getBody();
        return body;
    }
}

SpringSecurityConfig配置类

package cn.huangsong.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


/**
 *
 * @author: xxm
 * 功能描述: SpringSecurity的配置
 * @date: 2020/5/28 15:14
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled= true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 需要放行的URL
     */
    public static final String[] AUTH_WHITELIST = {
            "/login"
    };

    //密码编码器:
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    /**
     * 配置请求拦截
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()
                //由于使用的是JWT,我们这里不需要csrf
                .csrf().disable()
                //基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //可以匿名访问的链接
                .antMatchers(AUTH_WHITELIST).permitAll()
                //其他所有请求需要身份认证
                .anyRequest().authenticated()
                .and()
                //添加token验证过滤器
                .addFilter(new JWTAuthenticationFilter(authenticationManager()));
    }
}

1.4 test1服务导入需要的依赖

    <dependencies>
        <!--web依赖包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mysql依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>
        <!--mybatis-plus包-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-annotation</artifactId>
            <version>3.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.1.2</version>
        </dependency>
        <!--test1服务依赖-->
        <dependency>
            <groupId>cn.huangsong</groupId>
            <artifactId>test2</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

1.5 test1 结构


image.png

代码 TestController

package cn.huangsong.controller;

import cn.huangsong.config.JwtUtil;
import cn.huangsong.entity.TUser;
import cn.huangsong.service.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * @author huangsong
 * @title: TestController
 * @projectName single_sign_on
 * @description: TODO
 * @date 2022/2/189:22
 */
@RestController
public class TestController {

    @Autowired
    private UserDetailServiceImpl userControllerClient;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @RequestMapping("/test1")
    public String test1(){
        return "拦截失败";
    }
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public BasicAjaxResult toLogin(TUser user) {
        try {
            UserDetails userDetails = userDetailsService.loadUserByUsername(user.getLoginName());
            if (userDetails!=null) {
                String dbPassWord = userDetails.getPassword();
                if (passwordEncoder.matches(user.getPwd(),dbPassWord)) {
                    //创建token
                    String token = JwtUtil.generateToken(userDetails);
                    return new BasicAjaxResult().getSuccess("登陆成功").setResultObj(token);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return new BasicAjaxResult().getFail(e.getMessage());
        }
        return new BasicAjaxResult().getFail("用户名或密码错误");
    }    
}

代码UserDetailServiceImpl

package cn.huangsong.service;

import cn.huangsong.entity.TUser;
import cn.huangsong.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定义登录认证实现类
 * @author huangsong
 * @date 2021/3/18 14:10
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 加载数据库中的认证的用户的信息:用户名,密码,用户的权限列表
     * @param username: 该方法把username传入进来,我们通过username查询用户的信息
    (密码,权限列表等)然后封装成 UserDetails进行返回 ,交给security 。
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         //去数据库查询用户是否存在
        TUser TUser = userMapper.selectByUsername(username);
        if(TUser == null){
            throw new UsernameNotFoundException("无效的用户名");
        }
        //加载权限
        List<GrantedAuthority> permissions = new ArrayList<>();
        List<String> permission = new ArrayList<>();
        //测试
        permission.add("employee:add");
        permission.add("employee:update");
        if(permission.size()>0){
            for (String s : permission) {
                permissions.add(new SimpleGrantedAuthority(s));
            }
        }
        //密码是基于BCryptPasswordEncoder加密的密文
        //User是security内部的对象,UserDetails的实现类 ,
        //用来封装用户的基本信息(用户名,密码,权限列表)
        //四个true分别是账户启用,账户过期,密码过期,账户锁定
        return new User(username, TUser.getPassword(),true,true,true,true,permissions);
    }
}

2.测试
2.1过期的token/格式错误的token


image.png

image.png

image.png

2.2 直接访问test1接口,不携带token


image.png

2.3携带token访问


image.png

image.png

image.png

结束:在其他服务模块引入公共服务模块即可拦截过滤所有请求验证解析token。
以上是自己的理解+实现,有误的欢迎大家指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容