springsecurity 整合JWT

1.主要原理:首先登录,如果登录成功,则在响应的header中加入生成的jwt token。

                     然后客户在发起请求时,校验header中的token是否合法,如果合法放行,不合法则返回错误信息

2.依赖

        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.56</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.9</version>
		</dependency>

3.登录过滤器,生成token

import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StreamUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";
    private Logger log = LoggerFactory.getLogger(JWTLoginFilter.class);

    public JWTLoginFilter(AuthenticationManager authManager) {
        //配置只有访问/api/login时,这个过滤器才会起作用
        super(new AntPathRequestMatcher("/api/login", "POST"));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        JSONObject params;
        try {
            String jsonparam = StreamUtils.copyToString(request.getInputStream(), Charset.forName("utf-8"));
            params = JSONObject.parseObject(jsonparam);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            throw new AuthenticationServiceException("读取登录请求参数异常");
        }


        String username = null;
        String password = null;
        if (params != null) {
            username = params.getString("username");
            password = params.getString("password");
        }

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

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

        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
        TokenAuthenticationHandler tokenAuthenticationHandler = new TokenAuthenticationHandler();
        Object obj = auth.getPrincipal();
        if (obj != null) {
            UserDetails userDetails = (UserDetails) obj;
            String token = tokenAuthenticationHandler.generateToken(JSONObject.toJSONString(userDetails));
            res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + token);
            //todo 将最新生成的token保存在redis或是数据库中,可以在JWTAuthenticationFilter中判断提交上来的token和当前数据库中的token是否一致
            //todo ,如果不一致则说明客户端没有使用最新的token

        }
    }
}

4.验证token过滤器

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JWTAuthenticationFilter extends GenericFilterBean {
    static final String HEADER_STRING = "Authorization";
    static final String TOKEN_PREFIX = "Bearer";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;

        String token = req.getHeader(HEADER_STRING);
        if (StringUtils.isBlank(token) && req.getRequestURI().startsWith("/api")) {
            HttpServletResponse res = (HttpServletResponse) response;
            res.addHeader("Content-Type", "text/html;charset=UTF-8");
            res.setStatus(401);
            res.getWriter().write("{\"error\":\"请提供授权码\"}");
            return;
        }
        if (StringUtils.isNotBlank(token) && token.startsWith(TOKEN_PREFIX)) {
            //todo 从数据库中查询JWTLoginFilter生成的最新token,与当前提交的token对比,如果不一致,则返回token过期错误提示
            TokenAuthenticationHandler tokenAuthenticationHandler = new TokenAuthenticationHandler();
            String subject = tokenAuthenticationHandler.getSubjectFromToken(token.replace(TOKEN_PREFIX, ""));
            if (StringUtils.isBlank(subject)) {
                HttpServletResponse res = (HttpServletResponse) response;
                res.addHeader("Content-Type", "text/html;charset=UTF-8");
                res.setStatus(401);
                res.getWriter().write("{\"error\":\"授权码错误\"}");
                return;
            } else {
                //todo 校验token成功后,如果需要每次访问更新token,可以再次重新生成token,添加到响应头中
                //保存登录信息
                SecurityContextHolder.getContext().setAuthentication(new JWTAuthenticationToken(subject));
            }
        }
        filterChain.doFilter(request, response);
    }
}
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import static java.util.Collections.emptyList;

public class JWTAuthenticationToken extends UsernamePasswordAuthenticationToken {

    public JWTAuthenticationToken(Object principal) {
        super(principal,null,emptyList());
    }

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

    @Override
    public Object getPrincipal() {
        return super.getPrincipal();
    }
}
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

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

public class TokenAuthenticationHandler {

    private static final String CLAIM_KEY_CREATED = "created";
    private static final String CLAIM_KEY_SUBJECT = "subject";
    private static final String DEFAULT_SECRET = "@*secret@*";
    //有效期3天
    private static final Long DEFAULT_EXPIRATION = 259200L;

    public TokenAuthenticationHandler() {

    }

    public String getSubjectFromToken(String token) {
        String subject;
        try {
            final Claims claims = getClaimsFromToken(token);
            subject = claims.get(CLAIM_KEY_SUBJECT).toString();
        } catch (Exception e) {
            subject = null;
        }
        return subject;
    }

    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(DEFAULT_SECRET).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + DEFAULT_EXPIRATION * 1000);
    }

    public String generateToken(String subject) {
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put(CLAIM_KEY_CREATED, new Date());
        claims.put(CLAIM_KEY_SUBJECT, subject);
        return generateToken(claims);
    }

    public String generateToken(Map<String, Object> claims) {
        return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, DEFAULT_SECRET).compact();
    }
}

5.springsecurity配置

import com.example.demo.conf.security.JWTAuthenticationFilter;
import com.example.demo.conf.security.JWTLoginFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().passwordEncoder(passwordEncoder())
                .withUser("zhangsan").password("123456").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .formLogin().loginPage("/toLogin")//自定义登录页面
                .loginProcessingUrl("/login")//登录提交表单url
                .defaultSuccessUrl("/index")//登录成功后页面
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.addHeader("Content-Type", "text/html;charset=UTF-8");
                        if ("/api/login".equals(httpServletRequest.getRequestURI().toString())) {
                            httpServletResponse.getWriter().write("{\"error\":\"登录失败\"}");
                        } else {
                            httpServletResponse.getWriter().write("登录失败");
                        }
                    }
                })//登录失败处理
                .and().authorizeRequests()
                .antMatchers("/login").permitAll()//放行登录提交表单url
                .antMatchers("/toLogin").permitAll()//放行登录界面url
                .antMatchers("/api/login").permitAll()//放行api登录,获得jwt授权码
                .antMatchers("/**").authenticated() //所有路径都需要授权验证
                .and()
                .addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class)//api登录处理逻辑过滤器
                .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);//验证token是否合法过滤器
    }

    public JWTLoginFilter loginFilter() throws Exception {
        JWTLoginFilter loginFilter = new JWTLoginFilter(authenticationManager());
        loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
            response.setContentType("application/json");
            response.getWriter().write("{\"errormsg\":\"jwt认证失败\"}");
        });
        loginFilter.setContinueChainBeforeSuccessfulAuthentication(false);
        return loginFilter;
    }

    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return charSequence.toString().equals(s);
            }
        };
    }

}

项目源码地址:https://github.com/394938226/jwt_token

猜你喜欢

转载自blog.csdn.net/join_null/article/details/90601842