spring-security 与 jwt

1.前提

  • 已经了解了jwt的内容
  • 已经了解了oauth2的内容
  • 已经了解了spring-security基础知识
  • 有部分http协议的基础知识

2.为什么用jwt

用jwt主要解决前后端分离导致的2个问题

  • 跨域csrf问题

前提:前后端不分离时候解决跨域问题可以用token解决或者corf解决,前后端分离后由于访问后台本身就是跨域(ajax)所以解决方案失效

解决:jwt本身就是一个有某种规范的token,可以解决csrf问题

  • session问题

前提:前后端不分离的时候,每次访问请求游览器会自动携带cookie,cookie包含了jsessionId,用来维持session状态。当前后端分离的时候,ajax肯定是不会主动携带cookie信息的,所以我们可以通过设置前后端的头部信息达到一样的目的。但是虽然解决了session问题,csrf问题又没法解决了。

解决:jwt本身就是一个有某种规范的token,里面可以包括用户id,利用用户id可以维持某些状态,替代session

3.为什么不用oauth2

进行方案比较的时候一定要根据业务场景来使用,我觉得采用jwt的时候肯定是一个较为小型的应用,安全性能不会太高,能满足一定的认证与授权的要求,采用jwt的方案需要解决的一个问题是jwt的失效和续期问题。而如果采用oauth2的时候则是一个较为大型的认证授权框架,适合分布式、微服务、对外api等等,应用较大。

4.使用jwt的时候需要做的2个工作

  • 前台通过用户名和密码到达后台经过认证返回一个jwt-token

返回的jwt-token可以通过存储cookie或者header等等返回给客户端

  • 前台通过jwt-token到达后台解析认证信息

通过前台存储的jwt-token,一般存储在cookie跟着请求一起到达后台

5.开始spring-security-jwt的登陆操作

首先,服务器应用(下面简称“应用”)让用户通过Web表单将自己的用户名和密码发送到服务器的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。

实战 

通过继承AbstractAuthenticationProcessingFilter 创建一个认证过滤器,该过滤器只拦截login,通过回去传入的用户名和密码进行认证,并且重写了认证成功后的具体操作。

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
        SysUser creds = new ObjectMapper().readValue(req.getInputStream(), SysUser.class);
        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        creds.getUsername(),
                        creds.getPassword(),
                        Collections.emptyList()
                )
        );

    }
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
        TokenAuthenticationService.addAuthentication(res, auth.getName());
    }
}

6.应用和数据库核对用户名和密码。

实战

首先重写UserDetail对象,此对象的作用是方便UserDetailsService返回一个

public class SysUser implements UserDetails

接着重写一个UserDetailsService对象,返回上方重写的user对象。(此对象需要被传入DaoProviderManager进行用户认证) 

@Service
public class UserService implements UserDetailsService {
    @Autowired
    SysUserRepository sysUserRepository;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        SysUser user = sysUserRepository.findByUsername(s);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        return user;
    }

7.用户名和密码认证成功后

用户名和密码成功后,应用将用户的id(图中的user_id)作为JWT Payload的一个属性,将其与头部分别进行Base64编码拼接后签名,形成一个JWT。这里的JWT就是一个形同lll.zzz.xxx的字符串。

实战 

认证成功后将token放入cookie或者head返回,这里推荐放到cookie即可

class TokenAuthenticationService {
    static final long EXPIRATIONTIME = 1000*60*60*24*1; // 1 days
    static final String SECRET = "ThisIsASecret";
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";
    
    /**
     * 创建jwt
     * @param res
     * @param username
     */
    static void addAuthentication(HttpServletResponse res, String username) {
        String JWT = Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
//        Cookie cookie=new Cookie("llg","liliguang");
//        res.addCookie(cookie);
    }

8.设置httponly防止xss攻击

应用将JWT字符串作为该请求Cookie的一部分返回给用户。注意,在这里必须使用HttpOnly属性来防止Cookie被JavaScript读取,从而避免跨站脚本攻击(XSS攻击)

9.用户携带jwt-token访问

在Cookie失效或者被删除前,用户每次访问应用,应用都会接受到含有jwt的Cookie。从而应用就可以将JWT从请求中提取出来。

应用通过一系列任务检查JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。

应用在确认JWT有效之后,JWT进行Base64解码(可能在上一步中已经完成),然后在Payload中读取用户的id值,也就是user_id属性。这里用户的id为1025。

应用从数据库取到id为1025的用户的信息,加载到内存中,进行ORM之类的一系列底层逻辑初始化。

应用根据用户请求进行响应。

实战 

通过继承GenericFilterBean 创建一个过滤器,专门负责提取jwt-token并且解析之后将其构造成一个authenication放到SecurityContextHolder中。(SecurityContextHolder是一个多线程的用户安全上下文环境,负责维护用户状态)

由于spring-security的过滤器链没有从头部信息拿token的过滤器,所以我们需要创建一个并且添加到过滤器链中。之后通过request对象拿到jwt-token进行解析校验

public class JWTAuthenticationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        Authentication authentication = TokenAuthenticationService.getAuthentication((HttpServletRequest)request);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request,response);
    }
}
  static Authentication getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // parse the token.
            String user = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();

            return user != null ?
                    new UsernamePasswordAuthenticationToken(user, null, emptyList()) :
                    null;
        }
        return null;
    }

参考作者:John Wu
原文链接:http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/
版权归作者所有,转载请注明出处

参考作者:xjtushilei
原文链接:https://hacpai.com/article/1496982153867
版权归作者所有,转载请注明出处

代码 github 里,https://github.com/xjtushilei/spring-boot-security

猜你喜欢

转载自blog.csdn.net/m0_37834471/article/details/83144761