Spring Security+Redis实现登录

目录

前言

核心概念

环境配置

一、导入依赖

二、工具类

2.1、Reids工具类

2.2、token工具类

二、自定义UserDetails

三、相关配置类

2.1 、用户认证类

2.2、退出处理类

2.3、认证失败处理类

2.4、token认证过滤器

2.5、Spring Security配置类

2.5.1、JWT过滤器

2.5.2、 跨域处理

总结

前言

Spring Security是一个基于Spring框架的安全框架,它提供了一套完整的认证和授权机制,可以方便地实现登录功能

核心概念

1.认证(Authentication):验证用户的身份是否有效。
2.授权(Authorization):授予用户访问资源的权限。
3.安全上下文(Security Context):在应用程序执行期间,Spring Security将当前
  用户的安全信息存储在安全上下文中。
4.过滤器链(Filter Chain):Spring Security通过一组过滤器来拦截HTTP请求,并
  对请求进行认证和授权处理。
5.访问控制(Access Control):Spring Security可以根据配置的访问规则来限制用
  户对资源的访问。
6.Remember Me:记住用户的身份信息,使得用户在下次访问应用程序时不需要再次输入
  用户名和密码。
7.Session管理:Spring Security提供了对用户Session的管理机制,可以控制用户的
  登录和退出操作,并提供了一些额外的安全功能。

环境配置

在开始Spring Security之前,需要配置好以下环境:

1.JDK 1.8或以上
2.Maven 3.0或以上
3.Spring Boot 2.1.4或以上
4.IDE(推荐使用IntelliJ IDEA)

一、导入依赖

首先需要在Spring Boot项目中添加Spring Security的依赖。在pom.xml文件中添加以下依赖:

        <!-- spring security 安全认证 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- redis 缓存操作 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

二、工具类

2.1、Reids工具类

看我另一篇文章

2.2、token工具类

把处理token的一些操作封装起来了,直接拿去用就行

@Component
public class TokenService {
    /** 令牌秘钥 */
    @Value("${token.secretKey}")
    private String secretKey;

    protected static final long MILLIS_SECOND = 1000;
    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    @Resource
    private RedisCache redisCache;

    /**
     * 根据token获取登录用户
     * @param token
     */
    public Object getLoginUserByToken(String token) {
        if (!org.springframework.util.StringUtils.isEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        String tokenUuidKey = ConstantsCache.CACHE_KEY_TOKEN_LOGIN + SecurityUtils.decryptSecret(token, secretKey);
        Object loginUser = redisCache.getCacheObject(tokenUuidKey);
        // 获取到登录用户,说明用户有效,则redis续签
        if (loginUser != null) {
            GlobalConfig globalConfig = SpringUtil.getBean("globalConfig");
            long expireTime = Long.valueOf(globalConfig.getGlobalConfigMap().get("CORE_SYS_CONFIG:core.token.expireTime").toString());
            redisCache.expire(tokenUuidKey, expireTime, TimeUnit.MINUTES);
        }
        return loginUser;
    }

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtil.isNotEmpty(token)) {
            String tokenUuid = SecurityUtils.decryptSecret(token, secretKey);
            String userJson = redisCache.getCacheObject(getTokenUuidKey(tokenUuid));
            if (StringUtil.isNotBlank(userJson)) {
                return JSONObject.parseObject(userJson).toJavaObject(LoginUser.class);
            }
        }
        return null;
    }

    /**
     * 创建令牌
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createToken(LoginUser loginUser) {
        String tokenUuid = SeqUtil.fastUUID();
        String token = SecurityUtils.encryptSecret(tokenUuid, secretKey);
        loginUser.setTokenUuid(tokenUuid);
        loginUser.setToken(token);
        // 记录登录信息
        setUserBrowser(loginUser);
        // 登录用户存储到redis
        setRedisToken(loginUser);
        // 根据UUID生成Token
        return token;
    }

    /**
     * 写入令牌到redis
     * @param loginUser 登录信息
     */
    public void setRedisToken(LoginUser loginUser) {
        GlobalConfig globalConfig = SpringUtil.getBean("globalConfig");
        int expireTime = Integer.valueOf(globalConfig.getGlobalConfigMap().get("CORE_SYS_CONFIG:core.token.expireTime").toString());
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String tokenUuidKey = getTokenUuidKey(loginUser.getTokenUuid());
        redisCache.setCacheObject(tokenUuidKey, JSONObject.toJSONString(loginUser), expireTime, TimeUnit.MINUTES);
    }

    /**
     * 刷新令牌
     * @param loginUser
     */
    public void refreshRedisToken(LoginUser loginUser) {
        GlobalConfig globalConfig = SpringUtil.getBean("globalConfig");
        long expireTime = Long.valueOf(globalConfig.getGlobalConfigMap().get("CORE_SYS_CONFIG:core.token.expireTime").toString());
        // 根据uuid将loginUser缓存
        String tokenUuidKey = getTokenUuidKey(loginUser.getTokenUuid());
        redisCache.expire(tokenUuidKey, expireTime, TimeUnit.MINUTES);
    }

    /**
     * 删除令牌
     */
    public void delRedisToken(String tokenUuid) {
        if (StringUtil.isNotEmpty(tokenUuid)) {
            redisCache.deleteObject(getTokenUuidKey(tokenUuid));
        }
    }

    /**
     * 获取请求token
     * @param request
     * @return token
     */
    public String getToken(HttpServletRequest request) {
        String token = request.getHeader(Constants.TOKEN_HEADER);
        if (StringUtil.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    /**
     * 生成登录用户缓存key
     * @param tokenUuid
     * @return
     */
    private String getTokenUuidKey(String tokenUuid) {
        return ConstantsCache.CACHE_KEY_TOKEN_LOGIN + tokenUuid;
    }


    /**
     * 用户登录信息
     */
    private void setUserBrowser(LoginUser loginUser) {
        try {
            UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtil.getRequest().getHeader("User-Agent"));
            String ip = IpUtil.getIpAddr(ServletUtil.getRequest());
            loginUser.setIpaddr(ip);
            loginUser.setBrowser(userAgent.getBrowser().getName());
            loginUser.setOs(userAgent.getOperatingSystem().getName());
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
    }
}

二、自定义UserDetails

需要实现UserDetails接口,并重写其中方法和定义一下自己需要用到的属性。

@Data
public class LoginUser implements UserDetails {
    private static final long serialVersionUID = 1L;
    /** 用户唯一标识 */
    private String tokenUuid;
    private String token;
    /** 登陆时间 */
    private Long loginTime;
    /** 过期时间 */
    private Long expireTime;
    /** 登录IP地址 */
    private String ipaddr;
    /** 浏览器类型 */
    private String browser;
    /** 操作系统 */
    private String os;
    /** 用户信息 */
    private SysUser user;


    public LoginUser() {
    }
    public LoginUser(SysUser user) {
        this.user = user;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

}

三、相关配置类

2.1 、用户认证类

需要实现UserDetailsService接口,并重写其中的loadUserByUsername方法。在这个方法中,你可以从数据库或其他数据源中获取用户信息,并返回一个实现了UserDetails接口的对象。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询数据库需要更加实际情况修改
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), Collections.emptyList());
    }
}

2.2、退出处理类

需要实现LogoutSuccessHandler接口,并重写其中的onLogoutSuccess方法。在这个方法中,你可以对退出登录进行处理。

/**
 * 自定义退出处理类 返回成功
 *
 */
@Configuration
public class AuthTokenLogout implements LogoutSuccessHandler {
    @Resource
    private TokenService tokenService;

    /**
     * 退出处理
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtil.isNotNull(loginUser)) {
            // 删除用户缓存记录
            tokenService.delRedisToken(loginUser.getTokenUuid());
        }
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(JSON.toJSONString(AjaxResult.error(ContantsHttp.SUCCESS, "退出成功")));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.3、认证失败处理类

需要实现AuthenticationEntryPoint, Serializable接口,并重写其中的commence方法。在这个方法中,对认证失败的请求进行处理。


/**
 * 认证失败处理类 返回未授权
 */
@Component
public class AuthTokenEntryPoint implements AuthenticationEntryPoint, Serializable {
    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(JSON.toJSONString(AjaxResult.error(ContantsHttp.UNAUTHORIZED, "请求访问:" + request.getRequestURI() + ",认证失败,无法访问系统资源")));
    }
}

2.4、token认证过滤器

需要实现OncePerRequestFilter,并重写其中的doFilterInternal方法。在这个方法中,会拦截所有请求,在这里对请求是否携带token和是否在白名单做判断。


/**
 * token过滤器 验证token有效性
 */
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
    @Autowired
    private TokenService tokenService;
    @Autowired
    SessionRegistry sessionRegistry;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 白名单处理
        List<String> whiteList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(whiteList) && isSkipUrl(request.getRequestURI(), whiteList)) {
            chain.doFilter(request, response);
        } else {
            LoginUser loginUser = tokenService.getLoginUser(request);
            if (loginUser == null) {
                setResponse(response, 401, "401 未登录或登录已过期");
                return;
            }
            if (StringUtil.isNull(SecurityUtils.getAuthentication())) {
                tokenService.refreshRedisToken(loginUser);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
            chain.doFilter(request, response);
        }
    }

    /**
     * 判断当前访问的url是否开头URI是在配置的忽略url列表中
     */
    private boolean isSkipUrl(String url, List<String> whiteList) {
        for (String skipAuthUrl : whiteList) {
            if (url.startsWith(skipAuthUrl)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 设置响应信息
     */
    private void setResponse(HttpServletResponse response, int code, String msg) {
        try {
            byte[] responseByte = msg.getBytes(StandardCharsets.UTF_8);
            response.setStatus(code);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            PrintWriter writer = response.getWriter();
            writer.write(new String(responseByte));
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.5、Spring Security配置类

Spring Security和核心配置类,需要继承WebSecurityConfigurerAdapter类,在这里开启Spring Security并且做一些相关配置。

/**
 * spring security配置
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AuthTokenSecurity extends WebSecurityConfigurerAdapter {
    /** 自定义用户认证逻辑 */
    @Resource
    private UserDetailsService userDetailsService;
    /** 退出处理类 */
    @Resource
    private AuthTokenLogout logoutSuccessHandler;
    /** token认证过滤器 */
    @Resource
    private AuthTokenFilter authTokenFilter;
    /** 认证失败处理类 */
    @Resource
    private AuthTokenEntryPoint authTokenEntryPoint;
    /** 跨域过滤器 */
    @Resource
    private CorsFilter corsFilter;

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

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity .csrf().disable() .headers().frameOptions().disable();
        // 退出处理类
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 认证失败处理类
        httpSecurity.exceptionHandling().authenticationEntryPoint(authTokenEntryPoint);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, AuthTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);

        httpSecurity.sessionManagement().maximumSessions(1)
                .maxSessionsPreventsLogin(true)
                .sessionRegistry(sessionRegistry());
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

2.5.1、JWT过滤器

JWT是Json Web Token,也就是通过JSON的形式作为web应用中的令牌,用于在各方之间安全的将信息作为JSON对象传输,在传输过程中还可以完成数据加密、签名等相关处理。

在Spring Security配置类的configure方法添加

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity .csrf().disable() .headers().frameOptions().disable();
        // 退出处理类
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 认证失败处理类
        httpSecurity.exceptionHandling().authenticationEntryPoint(authTokenEntryPoint);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, AuthTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);

        httpSecurity.sessionManagement().maximumSessions(1)
                .maxSessionsPreventsLogin(true)
                .sessionRegistry(sessionRegistry());
    }

导入依赖

             <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
            </dependency>

2.5.2、 跨域处理

在Spring Security配置类中添加


    /**
     * 跨域配置
     */
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 设置访问源地址
        config.addAllowedOriginPattern("*");
        // 设置访问源请求头
        config.addAllowedHeader("*");
        // 设置访问源请求方法
        config.addAllowedMethod("*");
        // 对接口配置跨域设置
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

总结 

本文主要讲解如何通过Spring Security实现认证、过滤器、安全上下文,实现了基本的登录功能,希望以上内容对你有帮助!

JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。它是一种安全的传输方式,将用户的身份信息进行编码并生成一个令牌,可以在客户端和服务器之间进行传递。JWT通常由三部分组成:头部,载荷和签名。头部包含了令牌的类型和加密算法,载荷包含了用户的身份信息,签名用于验证令牌的合法性和完整性。 Spring Security是一个基于Spring框架的安全性解决方案,提供了一套全面的认证和授权机制。它集成了JWT作为一种认证方式,可以通过JWT来进行用户身份验证和授权。Spring Security可以提供用户认证、授权、密码加密、会话管理等功能。 Redis是一种内存数据库,它支持高性能的键值对存储,并提供了多种数据结构的支持。在商城系统中,Redis通常被用作缓存,用于存储用户的登录信息、购物车数据、商品库存等。通过将数据存储在内存中,Redis能够提供非常快速的读写性能,从而提升系统的响应速度和并发能力。 综上所述,JWT是一种用于身份验证和授权的开放标准,可以与Spring Security集成来实现安全认证和授权机制。而Redis则可以作为缓存数据库,用于提升系统性能和数据访问速度。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [基于SpringBoot2+MybatisPlus+SpringSecurity+jwt+redis+Vue的前后端商城系统源码](https://download.csdn.net/download/2301_76965813/87778818)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [SpringBoot2+MybatisPlus+SpringSecurity+jwt+redis+Vue的前后端分离的商城系统](https://download.csdn.net/download/weixin_47367099/85250567)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [通用权限管理系统+springboot+mybatis plus+spring security+jwt+redis+mysql](https://download.csdn.net/download/qq_37049128/87842802)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alpaca Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值