SpringSecurity + jwt 实现登录认证

SpringSecurity

SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。

JWT

JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。

JWT的组成

  • JWT token的格式:header.payload.signature
  • header中用于存放签名的生成算法
{"alg": "HS512"}
  • payload中用于存放用户名、token的生成时间和过期时间
{"sub":"admin","created":1489079981393,"exp":1489684781}
  • signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败
//secret为加密算法的密钥
String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

JWT实例

这是一个JWT的字符串

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.d-iki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw

可以在该网站上获得解析结果:https://jwt.io/
1.Web安全配置:

package com.auth.authserver.config;

import com.auth.authserver.filter.JwtLoginFilter;
import com.auth.authserver.filter.JwtVerifyFilter;
import com.auth.authserver.handle.MyAuthenticationEntryPoint;
import com.auth.authserver.handle.MyAccessDeniedHandle;
import com.auth.authserver.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author Json
 * @date 2021/10/29 15:08
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private PasswordEncoder passwordEncoder;
    private UserService userService;
    private MyAccessDeniedHandle myAccessDeniedHandle;
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @Autowired
    public void setRestfulAccessDeniedHandle(MyAccessDeniedHandle myAccessDeniedHandle) {
        this.myAccessDeniedHandle = myAccessDeniedHandle;
    }

    @Autowired
    public void setRestAuthenticationEntryPoint(MyAuthenticationEntryPoint myAuthenticationEntryPoint) {
        this.myAuthenticationEntryPoint = myAuthenticationEntryPoint;
    }

    @Bean
    public PasswordEncoder myPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * Remove the ROLE_ prefix
     */
    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults("");
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                // 允许访问
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated() // 其他请求拦截
                .and()
                .csrf().disable() //关闭csrf
                .addFilter(new JwtLoginFilter(super.authenticationManager()))
                .addFilter(new JwtVerifyFilter(super.authenticationManager()))
                .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandle) // 自定义无权限访问
                .authenticationEntryPoint(myAuthenticationEntryPoint) // 自定义未登录返回
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //禁用session
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // UserDetailsService类
        auth.userDetailsService(userService)
                // 加密策略
                .passwordEncoder(passwordEncoder);

    }

    /**
     * 解决 AuthenticationManager 无法注入的问题
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

2.定义接口UserService去继承UserDetailsService

/**
 * @author Json
 * @date 2021/10/29 15:11
 */
public interface UserService extends UserDetailsService {
    
}

3.定义实现类UserServiceImpl 实现UserService

package com.auth.authserver.service.impl;
import com.auth.authserver.service.UserService;
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.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Json
 * @date 2021/10/29 15:11
 */
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头
        authorities.add(new SimpleGrantedAuthority("ADMIN"));
        // 临时写死 这里是数据库查询出来的
        return User.builder().username("admin")
                .password(passwordEncoder.encode("123456"))
                .authorities(authorities).build();

    }
}

4.自定义用户名密码登录,也就是UsernamePasswordAuthenticationFilter,重写认证逻辑,其实也就是登录接口,默认地址为"/login" 请求为POST,这里面包含了认证,登录成功该怎么办,登录失败该怎么办,当然也可以自己去定义登录接口,其实到道理是一样的

package com.auth.authserver.filter;
import cn.hutool.json.JSONUtil;
import com.auth.authserver.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 登录校验
 *
 * 第一种方式 我们这里用框架自带的 过滤器实现
 * 第二种方式 可以自己实现登录接口  去认证 其实也是 AuthenticationManager。authenticate 去认证
 *
 * @author Json
 * @date 2021/11/1 13:47
 */
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    public JwtLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 相当于登录 认证
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        setDetails(request, authenticationToken);
        return authenticationManager.authenticate(authenticationToken);
    }

    /**
     * 一旦调用 springSecurity认证登录成功,立即执行该方法
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        //登录成功时,返回json格式进行提示
        String username = obtainUsername(request);
        String jwt = JwtUtils.createJwt(username);
        response.setContentType("application/json;charset=utf-8");
        Map<String, Object> map = new HashMap<>(4);
        // 这里写死只做测试  请以实际为主
        map.put("code", "200");
        map.put("message", "登陆成功!");
        map.put("token",jwt);
        response.addHeader("Authorization",  jwt);
        response.getWriter().println(JSONUtil.parse(map));
        response.getWriter().flush();
        response.getWriter().close();
    }

    /**
     * 一旦调用 springSecurity认证失败 ,立即执行该方法
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException {
        //登录失败时,返回json格式进行提示
        Map<String, Object> map = new HashMap<String, Object>(4);
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
        PrintWriter out = response.getWriter();
        if (ex instanceof BadCredentialsException) {
            map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
            map.put("message", "账号或密码错误!");
        }else {
            // 这里还有其他的 异常 。。 比如账号锁定  过期 等等。。。
            map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
            map.put("message", "登陆失败!");
        }
        out.write(new ObjectMapper().writeValueAsString(map));
        response.getWriter().println(JSONUtil.parse(map));
        response.getWriter().flush();
        response.getWriter().close();
    }
}

5.自定义请求拦截器,为什么呢,难道你可以随意请求吗?配置文件中写好了除了登录接口可以通过,其他请求全部拦截下来。这里是用jwt做的。

package com.auth.authserver.filter;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 请求校验
 *
 * @author Json
 * @date 2021/11/6 14:34
 */
public class JwtVerifyFilter extends BasicAuthenticationFilter {

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

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header != null) {
            List<GrantedAuthority> authorities = new ArrayList<>();
            // 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头
            authorities.add(new SimpleGrantedAuthority("user:resource"));
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken
                    ("admin",null, authorities);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
     
        }
        chain.doFilter(request, response);
    }
}

6.增加当访问接口没有权限时的处理

package com.auth.authserver.handle;
import cn.hutool.json.JSONUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 当访问接口没有权限时主要实现springsecurity给我们提供的 AccessDeniedHandler接口,自定义的返回结果
 *
 * @author Json
 * @date 2021/11/10 11:04
 */
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        // 这里写死只做测试  请以实际为主
        Map<String, Object> map = new HashMap<>();
        map.put("code", 501);
        map.put("msg", "您没有权限");
        response.getWriter().println(JSONUtil.parse(map));
        response.getWriter().flush();
    }
}

7.当未登录或者token失效访问接口时自定义处理

package com.auth.authserver.handle;
import cn.hutool.json.JSONUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 当未登录或者token失效访问接口时,自定义的返回结果
 *
 *
 * @author Json
 * @date 2021/11/10 11:20
 */
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Map<String, Object> map = new HashMap<>(4);
        // 这里写死只做测试  请以实际为主
        map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
        map.put("message", "请登录!");
        response.getWriter().println(JSONUtil.parse(map));
        response.getWriter().flush();
    }
}

附上jwt工具类,当然以实际为主,这里主要做测试,封装的很简单

package com.crm.common.utils;

import com.crm.common.config.JwtConfig;
import com.crm.common.enums.ResultCodeEnum;
import com.crm.common.exception.JwtApiException;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;

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

/**
 * jwt 工具类
 *
 * @author Json
 * @date 2021/11/15 16:38
 */
@Slf4j
public class JwtUtils {
    /**
     * jwt 载荷信息 key
     */
    public static final String JWT_PAYLOAD_USER_KEY = "user";

    /**
     * 刷新token次数 默认为0起始
     */
    public static final String REFRESH_TOKEN_NUMBER = "refreshTokenNumber";

    /**
     * access-token
     */
    public static final String ACCESS_TOKEN = "access-token";

    /**
     * 加密token
     *
     * @param userInfo  载荷中的数据
     * @param jwtConfig jwt 配置
     * @return JWT
     */
    public static String createAccessToken(Object userInfo, JwtConfig jwtConfig) {
        Map<String, Object> map = new HashMap<>();
        map.put(JWT_PAYLOAD_USER_KEY, userInfo);
        map.put(REFRESH_TOKEN_NUMBER, 0);
        return Jwts.builder()
                .setClaims(map)
                .setId(createJTI())
                .setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getAccessTokenExpire() * 1000))
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getAccessTokenSecret())
                .compact();
    }


    /**
     * 生成 RefreshToken
     *
     * @return refreshToken
     */
    public static String createRefreshToken(Object userInfo, JwtConfig jwtConfig) {
        return createRefreshToken(userInfo, jwtConfig, 0);
    }


    /**
     * 生成 RefreshToken
     *
     * @return refreshToken
     */
    public static String createRefreshToken(Object userInfo, JwtConfig jwtConfig, int refreshTokenNumber) {
        Map<String, Object> map = new HashMap<>();
        map.put(JWT_PAYLOAD_USER_KEY, userInfo);
        map.put(REFRESH_TOKEN_NUMBER, refreshTokenNumber);
        return Jwts.builder()
                .setClaims(map)
                .setId(createJTI())
                .setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getRefreshTokenExpire() * 1000))
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getRefreshTokenSecret())
                .compact();
    }


    /**
     * 解析 refreshToken
     *
     * @param token     token
     * @param jwtConfig 配置项
     * @return 载荷信息
     */
    public static Claims parserAccessToken(String token, JwtConfig jwtConfig) {
        return parserToken(token, jwtConfig.getAccessTokenSecret());
    }

    /**
     * 解析  token
     *
     * @param token     token
     * @param jwtConfig 配置项
     * @return 载荷信息
     */
    public static Claims parserRefreshToken(String token, JwtConfig jwtConfig) {
        return parserToken(token, jwtConfig.getRefreshTokenSecret());
    }

    /**
     * 获取token中的载荷信息
     *
     * @param token 用户请求中的令牌
     * @return 用户信息
     */
    public static Claims parserToken(String token, String secretKey) {
        try {
            return Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.error("token{}过期", token, e);
            throw new JwtApiException(ResultCodeEnum.JWT_EXPIRED.code(), ResultCodeEnum.JWT_EXPIRED.message());
        } catch (SignatureException e) {
            log.error("token=[{}], 签名", token, e);
            throw new JwtApiException(ResultCodeEnum.JWT_SIGNATURE.code(), ResultCodeEnum.JWT_SIGNATURE.message());
        } catch (Exception e) {
            log.error("token=[{}]解析错误 message:{}", token, e.getMessage(), e);
            throw new JwtApiException(ResultCodeEnum.JWT_ERROR.code(), ResultCodeEnum.JWT_ERROR.message());
        }
    }


    private static String createJTI() {
        return new String(java.util.Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容