IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringBoot + SpringSecurity + JWT 实现后端校验 -> 正文阅读

[Java知识库]SpringBoot + SpringSecurity + JWT 实现后端校验

集成了SpringSecurity后,又花了一天时间集成了JWT,记录一下。
完整代码在我的Gitee仓库:FreeFancy

0、一些背景

HTTP是无状态的,我们不能确定两次请求是不是一个用户发出的,就必须要每次都进行认证。传统方法解决这个问题是采用session和cookie机制,虽然解决了问题,但有一些缺点:

  1. session通常是保存在内存中的,用户数量如果较多,服务器的压力大;
  2. 因为session是保存在最初认证的那台服务器上,换了一台服务器又需要认证,不能很好的适应分布式的环境;
  3. 有CSRF的风险。因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

后来人们提出了基于token的鉴权机制,它不需要服务器去记录用户的认证或会话信息,而是将token交给客户端保存,每次的请求的时候携带,服务器只要鉴定即可,这样做有一些优点:

  1. 服务器开销小;
  2. 不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利;
  3. 因为不依赖于session,就杜绝了CSRF的风险。

基于Token的鉴权机制应用最广的还是JWT(JSON WEB TOKEN)。这里我们不多解释JWT,相关文档很多请自动搜索。

1、实战

前置工作

这里是在SpringBoot已经简单整合了SpringSecurity的情况下开始整合JWT,这里前置工作有:
因为是前后端分离的情况,我们统一返回JSON,所以实现了

  • 登录成功的自定义处理;
  • 登录失败的自定义处理;
  • 未登录被拦截的自定义处理。

我们的用户信息是要在数据库中取的,所以我们要实现UserDetailsService接口,返回一个保存了用户信息的UserDetails对象,并在配置中注册。

部分配置文件

@Override
    protected void configure(HttpSecurity http) throws Exception {
        // 未登录拦截处理
        http.exceptionHandling()
                .authenticationEntryPoint(customizeAuthenticationEntryPoint);
        // 登录
        http.formLogin()
                //登录成功处理逻辑
                .successHandler(customizeAuthenticationSuccessHandler)
                //登录失败处理逻辑
                .failureHandler(customizeAuthenticationFailureHandler);
        // 登出
        http.logout()
                //登出成功处理逻辑
                .logoutSuccessHandler(customizeLogoutSuccessHandler);
        // 认证
        http.authorizeRequests()
                // 公共接口放行
                .antMatchers("/login", "/register").anonymous()
                // swagger静态资源放行
                .antMatchers(SWAGGER_SOURCE_PERMIT_ALL.split(",")).permitAll()
                .anyRequest().authenticated();
    }

这里贴出其中一个自定义处理,其他都是类似的,返回相应结果的JSON信息。

//登录被拦截自定义的处理
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
        ResponseJson responseJson = ResponseJson.failure(ResponseCode.USER_NOT_LOGGED_IN);
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(responseJson));
    }
}

导包

<!-- jwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

思路

看了很多别人的博客,思路并不清晰,开始还以为这东西搞起来很复杂,最后弄下其实还是不难的,需要配置的东西并不多。

  1. 对于新登录的用户,我们只要在登录成功后将TOKEN返回!,原本的逻辑并不需要修改,只要在上面我们自定义登录成功的处理中返回生成好的TOKEN。
  2. 对于登录过的用户发出的携带TOKEN的请求,我们要给它放行。
  3. 对于没有携带TOKEN的用户请求(要拦截的路径),给它打回去。
  4. JWT TOKEN的生成和解析

思路是很自然的,得益于SpringSecurity实现起来也不难。

1、登录成功后返回TOKEN

首先,/login路径的请求是不需要携带TOKEN就可以访问的。

http.authorizeRequests()
                // 公共接口放行
                .antMatchers("/login", "/register").anonymous()

如果是表单登录,我们可以直接使用SpringSecurity自带的登录处理,请求参数中携带username,password,请求发给/login就可以了。
还记得我们的登录成功自定义处理嘛?我们在里面把token生成然后返回就可以了。

// 登录
        http.formLogin()
                //登录成功处理逻辑
                .successHandler(customizeAuthenticationSuccessHandler)
                //登录失败处理逻辑
                .failureHandler(customizeAuthenticationFailureHandler);
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
    	// 这里是生成TOKEN返回,有些方法还没讲,先理思路,知道是生成TOKEN即可!
    	JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        StringBuilder authoritiesStr = new StringBuilder();
        for (GrantedAuthority authority : authorities) {
            authoritiesStr.append(authority.toString());
            authoritiesStr.append(JwtTokenUtils.SPLIT_CHAR);
        }
        String token = JwtTokenUtils.createToken(jwtUser.getId(), jwtUser.getUsername(), authoritiesStr.toString());
        //返回json数据
        HashMap<String, String> hashMap = new HashMap<>(1);
        hashMap.put("Authorization", token);
        ResponseJson responseJson = ResponseJson.success(hashMap);
        //处理编码方式,防止中文乱码的情况
        httpServletResponse.setContentType("text/json;charset=utf-8");
        //塞到HttpServletResponse中返回给前台
        httpServletResponse.getWriter().write(JSON.toJSONString(responseJson));
    }
}

这样我们就完成了第一步。

2、携带TOKEN的请求,进行放行

SpringSecurity本质是一个拦截器连(Filter Chain),我们需要自定义一个拦截器取处理请求中的TOKEN,并为请求放行。

/**
 * @author LiMing
 * @date 2021/11/18 19:49
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String headerToken = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        //如果请求头中没有token直接放行了,交给后续的拦截器处理。
        if(headerToken == null || !headerToken.startsWith(JwtTokenUtils.TOKEN_PREFIX)){
            chain.doFilter(request, response);
            return;
        }
        //请求头中有token且没过期
        if(!JwtTokenUtils.isExpiration(headerToken.replace(JwtTokenUtils.TOKEN_PREFIX,""))){
            //设置上下文,将认证的信息放进去,告诉后面的Filter不用再鉴权了!
            CustomizeUsernamePasswordAuthenticationToken authentication = getAuthentication(headerToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        super.doFilterInternal(request, response, chain);
    }
	//该方法就是从TOKNE中解析出用户的认证和鉴权信息
    private CustomizeUsernamePasswordAuthenticationToken getAuthentication(String headerToken){
        String token = headerToken.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        Long userId = JwtTokenUtils.getUserId(token);
        String username = JwtTokenUtils.getUserName(token);
        // 设置权限
        String authoritiesStr = JwtTokenUtils.getUserAuthority(token);
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String s : authoritiesStr.split(JwtTokenUtils.SPLIT_CHAR)) {
            if (s != null && !"".equals(s)) {
                authorities.add(new SimpleGrantedAuthority(s));
            }
        }
        if(username != null){
            return new CustomizeUsernamePasswordAuthenticationToken(userId, username, null, authorities);
        }
        return null;
    }
}

我们要将这个Filter注册进SpringSecurity里,框架留有接口,很方便,但我们应该放在哪里呢?

// JWT
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);

UsernamePasswordAuthenticationFilter是用来登录认证的,我们放在它前面,将TOKEN解析出的用户认证信息写进SecurityContext里,后续就可以放行了。
另外这里提一嘴,因为基于TOKEN的认证鉴权不依赖于session,所以我们可以把session禁用了,当然也可以大胆的把csrf防护关闭。

// 禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

http.csrf().disable();

3、拦截对于未携带TOKEN的请求

不是登录请求,又未携带TOKEN信息,会被后续的拦截器拦截,肯定是进不到系统的,后续拦截器报错,我们只要处理异常即可。
这里甚至不用特意处理,因为这和之前未登录拦截处理是一样的。

// 未登录拦截处理
        http.exceptionHandling()
                .authenticationEntryPoint(customizeAuthenticationEntryPoint);
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
        ResponseJson responseJson = ResponseJson.failure(ResponseCode.USER_NOT_LOGGED_IN);
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(responseJson));
    }
}

4、生成和解析TOKEN

生成是在成功登录后返回,解析是在处理携带TOKEN的请求,解析出用户的认证信息。
那哪些信息要放在TOKEN里呢?username和authorities(权限信息)以及你想要的信息。
我们对UserDetailsService的实现要求loadUserByUsername()返回一个UserDetails对象。

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

}

自带的对象就可以,它是UserDetails的实现类。

org.springframework.security.core.userdetails.User

但它不能保存用户的ID,我就想把ID保存进TOKEN里,下次携带的时候可以解析出来,那我就自己写一个实现类。

/**
 * @author LiMing
 * @date 2021/11/19 11:40
 */
public class JwtUser implements UserDetails {
    private final Long id;
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;

    public JwtUser(Long id, String username, Set<GrantedAuthority> authorities){
        this.id = id;
        this.username = username;
        this.authorities = authorities;
    }

    public JwtUser(Long id, String username, String password, Set<GrantedAuthority> authorities){
        this.id = id;
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    public Long getId(){
        return id;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities(){
        return authorities;
    }

    @Override
    public String getPassword(){
        return password;
    }

    @Override
    public String getUsername(){
        return username;
    }

    @Override
    public boolean isAccountNonExpired(){
        return true;
    }

    @Override
    public boolean isAccountNonLocked(){
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired(){
        return true;
    }

    @Override
    public boolean isEnabled(){
        return true;
    }
}

这个对象里的信息,就是我们要保存在TOKEN里的信息。
然后我们需要生成和解析TOKEN的一个工具类,这里直接给出我的。

/**
 * @author LiMing
 * @date 2021/11/18 17:22
 */
public class JwtTokenUtils {
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String SECRET = "idea@whut.edu.cn";
    public static final String SPLIT_CHAR = ";";

    /**
     * 过期时间3小时
     */
    private static final Long EXPIRATION = 60 * 60 * 3L;

    private static final String AUTHORITY = "authority";
    private static final String ID = "id";

    public static String createToken(Long id, String username, String authority){
        HashMap<String, Object> map = new HashMap<>(2);
        map.put(ID, id);
        map.put(AUTHORITY, authority);
        return TOKEN_PREFIX + Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setClaims(map)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .compact();
    }

    /**
     * 解析用户Id
     * @param token jwt token
     * @return id
     */
    public static Long getUserId(String token){
        Long id;
        try {
            id = (Long) getTokenBody(token).get(ID);
        } catch (Exception e){
            id = null;
        }
        return id;
    }

    /**
     * 从token中获取用户名(此处的token是指去掉前缀之后的)
     * @param token jwt token
     * @return username
     */
    public static String getUserName(String token){
        String username;
        try {
            username = getTokenBody(token).getSubject();
        } catch (Exception e){
            username = null;
        }
        return username;
    }

    public static String getUserAuthority(String token){
        return (String) getTokenBody(token).get(AUTHORITY);
    }

    private static Claims getTokenBody(String token){
        return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
    }

    /**
     * 是否已过期
     * @param token jwt token
     * @return 是/否
     */
    public static boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }
}

生成TOKEN用在:

@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
        JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        StringBuilder authoritiesStr = new StringBuilder();
        for (GrantedAuthority authority : authorities) {
            authoritiesStr.append(authority.toString());
            authoritiesStr.append(JwtTokenUtils.SPLIT_CHAR);
        }
        String token = JwtTokenUtils.createToken(jwtUser.getId(), jwtUser.getUsername(), authoritiesStr.toString());
        //返回json数据
        HashMap<String, String> hashMap = new HashMap<>(1);
        hashMap.put("Authorization", token);
        ResponseJson responseJson = ResponseJson.success(hashMap);
        //处理编码方式,防止中文乱码的情况
        httpServletResponse.setContentType("text/json;charset=utf-8");
        //塞到HttpServletResponse中返回给前台
        httpServletResponse.getWriter().write(JSON.toJSONString(responseJson));
    }
}

解析TOKEN用在:

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String headerToken = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        //如果请求头中没有token直接放行了
        if(headerToken == null || !headerToken.startsWith(JwtTokenUtils.TOKEN_PREFIX)){
            chain.doFilter(request, response);
            return;
        }
        //请求头中有token且没过期
        if(!JwtTokenUtils.isExpiration(headerToken.replace(JwtTokenUtils.TOKEN_PREFIX,""))){
            //设置上下文
            CustomizeUsernamePasswordAuthenticationToken authentication = getAuthentication(headerToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        super.doFilterInternal(request, response, chain);
    }

    private CustomizeUsernamePasswordAuthenticationToken getAuthentication(String headerToken){
        String token = headerToken.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        Long userId = JwtTokenUtils.getUserId(token);
        String username = JwtTokenUtils.getUserName(token);
        // 设置权限
        String authoritiesStr = JwtTokenUtils.getUserAuthority(token);
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String s : authoritiesStr.split(JwtTokenUtils.SPLIT_CHAR)) {
            if (s != null && !"".equals(s)) {
                authorities.add(new SimpleGrantedAuthority(s));
            }
        }
        if(username != null){
            return new CustomizeUsernamePasswordAuthenticationToken(userId, username, null, authorities);
        }
        return null;
    }
}

因为SecurityContextHolder.getContext().setAuthentication();要求传入Authentication的实现了,但框架自带的并不能携带我们要求携带的userId,所以自定义一个实现类。

/**
 * @author LiMing
 * @date 2021/11/19 18:11
 */
public class CustomizeUsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

    private final Long userId;

    private final Object principal;

    private Object credentials;

    public CustomizeUsernamePasswordAuthenticationToken(Long userId, Object principal, Object credentials) {
        super(null);
        this.userId = userId;
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    public CustomizeUsernamePasswordAuthenticationToken(Long userId, Object principal, Object credentials,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.userId = userId;
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    public Long getUserId() {
        return this.userId;
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

5、使用SecurityContext中的信息

每次请求,SecurityContext中保有我们放入的一小段信息,这份信息可以在该次请求的时候拿出来。

/**
 * @author LiMing
 * @date 2021/11/19 17:53
 */
public class SecurityUtils {
    public static Long getUserId(){
        Object token = SecurityContextHolder.getContext().getAuthentication();
        if (token instanceof CustomizeUsernamePasswordAuthenticationToken) {
            return ((CustomizeUsernamePasswordAuthenticationToken) token).getUserId();
        }
        return null;
    }
}

到这里就完成了集成,捋清了思路其实还是很简单的,可以根据业务调整的地方有很多,可以在理解原理后按需修改。
如有错误,恳请指出!

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-20 18:15:31  更:2021-11-20 18:17:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:03:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码