spring security+jwt+redis

  • Post author:
  • Post category:其他


首先,我们知道springsecurity基于会话进行安全管理,我们要实现无状态的安全框架就不能基于会话。所以加入jwt,spring security我们用它只是用它所拥有的各种过滤器来完成我们的功能。



1.依赖导入

使用springboot进行开发:

依赖



2.过滤器

过滤器在项目中是很重要的,尤其是安全框架,我们看看spring security的过滤器顺序,后续我们会根据过滤器开发。

过滤器顺序



3.思路



在认证过滤器之前,我们自定义两个过滤器

1.

JwtLoginFilter

用来拦截登录请求进行成功与失败处理,继承

UsernamePasswordAuthenticationFilte

过滤器(当然登录成功失败处理可以自定义,加此过滤器可以做一下额外处理)

2.

JwtAuthenticationFilte

拦截请求并判断token,继承

BasicAuthenticationFilter

此过滤器针对的是拥有Authorization头部信息的请求 咋们的token就在此存储,继承此过滤器对token进行处理



JwtLoginFilter

package com.guidezhilv.guideconsolewebboot.security;
import java.io.*;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.guidezhilv.guideconsolewebboot.model.User;
import com.guidezhilv.guideconsolewebboot.security.loginFail.MyAuthenticationFailureHandler;
import com.guidezhilv.guideconsolewebboot.security.loginSuccess.MyAuthenticationSuccessHandler;
import com.guidezhilv.guideconsolewebboot.service.UserService;
import com.guidezhilv.guideconsolewebboot.service.impl.SysUserServiceImpl;
import com.guidezhilv.guideconsolewebboot.utils.JwtTokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * 启动登录认证流程过滤器
 * @author Louis
 * @date Jun 29, 2019
 */
@Component
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {




    public JwtLoginFilter(AuthenticationManager authManager) {
        setAuthenticationManager(authManager);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        // POST 请求 /login 登录时拦截, 由此方法触发执行登录认证流程,可以在此覆写整个登录认证逻辑
        super.doFilter(req, res, chain);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 可以在此覆写尝试进行登录认证的逻辑,登录成功之后等操作不再此方法内
        // 如果使用此过滤器来触发登录认证流程,注意登录请求数据格式的问题
        // 此过滤器的用户名密码默认从request.getParameter()获取,但是这种
        // 读取方式不能读取到如 application/json 等 post 请求数据,需要把
        // 用户名密码的读取逻辑修改为到流中读取request.getInputStream()

        String body = getBody(request);
        JSONObject jsonObject = JSON.parseObject(body);
        String username = jsonObject.getString("username");
        String password = jsonObject.getString("password");

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();

        JwtAuthenticatioToken authRequest = new JwtAuthenticatioToken(username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);

    }

    @Override
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        // 存储登录认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authResult);
        // 记住我服务
//        getRememberMeServices().loginSuccess(request, response, authResult);
        // 触发事件监听器
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }
        // 生成并返回token给客户端,后续访问携带此token
        Object principal = authResult.getPrincipal();
        String username = ((UserDetails) principal).getUsername();
        SysUserServiceImpl userDetailsService=new SysUserServiceImpl();
        User byUsername = userDetailsService.findByUsername(username);

        JwtAuthenticatioToken token = new JwtAuthenticatioToken(null, null, JwtTokenUtils.generateToken(authResult,byUsername.getId()+""));
        token.setUserKey(byUsername.getId());
        MyAuthenticationSuccessHandler myAuthenticationSuccessHandler = new MyAuthenticationSuccessHandler();
        myAuthenticationSuccessHandler.onAuthenticationSuccess(request,response,token);
    }
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        MyAuthenticationFailureHandler myAuthenticationFailureHandler = new MyAuthenticationFailureHandler();
        myAuthenticationFailureHandler.onAuthenticationFailure(request,response,failed);
    }
    /**
     * 获取请求Body
     * @param request
     * @return
     */
    public String getBody(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}




JwtAuthenticationFilte

package com.guidezhilv.guideconsolewebboot.security;

import java.io.IOException;

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

import com.guidezhilv.guideconsolewebboot.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;


/**
 * 登录认证检查过滤器
 * @author Louis
 * @date Jun 29, 2019
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    @Autowired
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {

        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 获取token, 并检查登录状态  调用工具类判断token
        boolean state = SecurityUtils.checkAuthentication(request);
        if (!state){
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            response.getWriter().println("{\"state\":207,\"msg\":\"token过期重新登录\"}");
            response.getWriter().flush();
        }else{
            chain.doFilter(request, response);
        }


    }

}



登录成功与失败处理

在前后端分离中,指定成功失败页面耦合性太强,所以登录成功失败处理 直接返回json 数据。况且在jwt中为了保证token的安全性,登录成功会将token存入redis,以保证无状态。

在上文提到

JwtLoginFilter

过滤,重写方法中有失败和成功的处理方法

successfulAuthentication

,

unsuccessfulAuthentication

通过重写这两个方法可以返回指定数据。


@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private  static RedisTemplate redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate redisTemplate){
        MyAuthenticationSuccessHandler.redisTemplate=redisTemplate;
    }
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws ServletException, IOException {
        //将用户id和token存入redis
        if (authentication instanceof JwtAuthenticatioToken){
            JwtAuthenticatioToken authentication1 = (JwtAuthenticatioToken) authentication;
            ValueOperations valueOperations = redisTemplate.opsForValue();
            valueOperations.set(authentication1.getUserKey()+"",authentication1.getToken(),60*60*2, TimeUnit.SECONDS);
        }

        //例1:不跳到XML设定的页面,而是直接返回json字符串
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        JwtAuthenticatioToken authentication1 = (JwtAuthenticatioToken) authentication;
        response.getWriter().println("{\"state\":\"200\",\" msg\":\"登录成功\",\" token\":"+authentication1.getToken()+"}");

    }
};



MyAuthenticationFailureHandler

public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        Example1(request, response, exception);
//		Example2(request,response,exception);
//        Example3(request,response,exception);
    }

    private void Example1(HttpServletRequest request, HttpServletResponse response,
                          AuthenticationException exception) throws IOException, ServletException {
        System.out.println(exception.getClass());
        if (exception instanceof BadCredentialsException) {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            response.getWriter().println("{\"state\":205,\"msg\":\"密码错误\"}");
        } else if (exception instanceof AuthenticationServiceException) {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            response.getWriter().println("{\"state\":206,\"msg\":\"没有此用户\"}");
        } else {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            response.getWriter().println("{\"state\":204,\"msg\":\"登录失败\"}");
        }
        //例1:直接返回字符串

    }

    private void Example2(HttpServletRequest request, HttpServletResponse response,
                          AuthenticationException exception) throws IOException, ServletException {
        String strUrl = request.getContextPath() + "/customLoginResponse.jsp";
        request.getSession().setAttribute("ok", 0);
        request.getSession().setAttribute("message", exception.getLocalizedMessage());
        request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
        super.onAuthenticationFailure(request, response, exception);
    }

    private void Example3(HttpServletRequest request, HttpServletResponse response,
                          AuthenticationException exception) throws IOException, ServletException {
        //例3:自定义跳转到哪个URL
        //假设login.jsp在webapp路径下
        //注意:不能访问WEB-INF下的jsp。
        String strUrl = request.getContextPath() + "/customLoginResponse.jsp";
        request.getSession().setAttribute("ok", 0);
        request.getSession().setAttribute("message", exception.getLocalizedMessage());
        request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
        //Error  request.getRequestDispatcher(strUrl).forward(request, response);
        response.sendRedirect(strUrl);
    }
};



授权工具类



JwtAuthenticationFilter

中用来校验token的状态 比如 token过期,token是否正确。



SecurityUtils

public class SecurityUtils {

    /**
     * 系统登录认证
     * @param request
     * @param username
     * @param password
     * @param authenticationManager
     * @return
     */
    public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
        JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
        token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        // 执行登录认证过程
        Authentication authentication = authenticationManager.authenticate(token);
        // 认证成功存储认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 生成令牌并返回给客户端
//        token.setToken(JwtTokenUtils.generateToken(authentication));
        return token;
    }

    /**
     * 获取令牌进行认证
     * @param request
     */
    public static boolean checkAuthentication(HttpServletRequest request) throws IOException {
        // 获取令牌并根据令牌获取登录认证信息
        Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
        // 设置登录认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);

        if (authentication==null){
            return false;
        }
        return true;
    }

    /**
     * 获取当前用户名
     * @return
     */
    public static String getUsername() {
        String username = null;
        Authentication authentication = getAuthentication();
        if(authentication != null) {
            Object principal = authentication.getPrincipal();

            if(principal != null && principal instanceof UserDetails) {
                username = ((UserDetails) principal).getUsername();
            }
        }
        return username;
    }

    /**
     * 获取用户名
     * @return
     */
    public static String getUsername(Authentication authentication) {
        String username = null;
        if(authentication != null) {
            Object principal = authentication.getPrincipal();
            if(principal != null && principal instanceof UserDetails) {
                username = ((UserDetails) principal).getUsername();
            }
        }
        return username;
    }

    /**
     * 获取当前登录信息
     * @return
     */
    public static Authentication getAuthentication() {
        if(SecurityContextHolder.getContext() == null) {
            return null;
        }
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication;
    }

}



JwtTokenUtils

public class JwtTokenUtils implements Serializable {

    private static final long serialVersionUID = 1L;


    private static RedisTemplate redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate redisTemplate){
        JwtTokenUtils.redisTemplate=redisTemplate;
    }
    /**
     * 用户名称
     */
    private static final String USERNAME = Claims.SUBJECT;
    /**
     * 创建时间
     */
    private static final String CREATED = "created";
    /**
     * 权限列表
     */
    private static final String AUTHORITIES = "authorities";
    /**
     * 用户id
     */
    private static final String USERKEY = "userkey";
    /**
     * 密钥
     */
    private static final String SECRET = "abcdefgh";
    /**
     * 有效期12小时  12 * 60 *60 * 1000
     */
    private static final long EXPIRE_TIME = 12*60*60* 1000;

    /**
     * 生成令牌
     *
     * @param
     * @return 令牌
     */
    public static String generateToken(Authentication authentication, String id) {
        Map<String, Object> claims = new HashMap<>(3);
        claims.put(USERNAME, SecurityUtils.getUsername(authentication));
        claims.put(CREATED, new Date());
        claims.put(USERKEY, id);
        claims.put(AUTHORITIES, authentication.getAuthorities());
        return generateToken(claims);
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private static String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public static String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 根据请求令牌获取登录认证信息
     *
     * @param
     * @return 用户名
     */
    public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
        Authentication authentication = null;
        // 获取请求携带的令牌
        String token = JwtTokenUtils.getToken(request);
        if (token != null) {
            // 请求令牌不能为空
            if (SecurityUtils.getAuthentication() == null) {
            if (isTokenExpired(token)) {
                return null;
            }
                if (!valRedisToken(token, getUserKeyByToken(token)+"")) {
                   return null;
                }
                // 上下文中Authentication为空
                Claims claims = getClaimsFromToken(token);
                if (claims == null) {
                    return null;
                }
                String username = claims.getSubject();
                if (username == null) {
                    return null;
                }

                Object authors = claims.get(AUTHORITIES);
                List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
                if (authors != null && authors instanceof List) {
                    for (Object object : (List) authors) {
                        authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
                    }
                }

                authentication = new JwtAuthenticatioToken(new JwtUserDetails(username, "", authorities), null, authorities, token);
            } else {
                if (validateToken(token, SecurityUtils.getUsername())) {
                    // 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
                    authentication = SecurityUtils.getAuthentication();
                }
            }
        }
        return authentication;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 验证令牌
     *
     * @param token
     * @param username
     * @return
     */
    public static Boolean validateToken(String token, String username) {
        String userName = getUsernameFromToken(token);
        if (valRedisToken(token, getUserKeyByToken(token)+"")) {
            if (StringUtils.isEmpty(userName)) {
                return false;
            }
            return (userName.equals(username) && !isTokenExpired(token));
        }
        return false;

    }

    /*
     * 用token获取用户key
     * */
    public static Long getUserKeyByToken(String token) {
        Claims claimsFromToken = getClaimsFromToken(token);
        Long userKey = Long.parseLong( claimsFromToken.get(USERKEY)+"");
        return userKey;
    }

    /*
     * 判断redis中是否有token及是否过期
     * */
    public  static Boolean valRedisToken(String token, String userKey) {
        //查询redis中的token
        if (redisTemplate.hasKey(userKey)) {
            ValueOperations valueOperations = redisTemplate.opsForValue();
            String redisToken = (String) valueOperations.get(userKey);
            if (StringUtils.isNotBlank(redisToken) && token.equals(redisToken)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 刷新令牌
     *
     * @param token
     * @return
     */
    public static String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put(CREATED, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public static Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            boolean before = expiration.before(new Date());
            return before;
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 获取请求token
     *
     * @param request
     * @return
     */
    public static String getToken(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        String tokenHead = "Bearer ";
        if (token == null) {
            token = request.getHeader("token");
        } else if (token.contains(tokenHead)) {
            token = token.substring(tokenHead.length());
        }
        if ("".equals(token)) {
            token = null;
        }
        return token;
    }


}



对于认证登录信息,使用的安全框架默认的认证方法,你可以实现UserDetailsService重写loadUserByUsername方法来完成登录信息与数据库信息认证

public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("该用户不存在");
        }
        // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
        Set<String> permissions = userService.findPermissions(username);
        List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
        return new JwtUserDetails(username, user.getPassword(), grantedAuthorities);
    }
}



4.结尾

此篇文章只是大概说明使用方法,详情代码


github代码详情



版权声明:本文为qq_42671464原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。