SpringSecurity + jwt 实现登录认证

作者: 晚风吹___ | 来源:发表于2021-11-10 15:44 被阅读0次

    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()));
        }
    }
    
    

    相关文章

      网友评论

        本文标题:SpringSecurity + jwt 实现登录认证

        本文链接:https://www.haomeiwen.com/subject/kkfizltx.html