我是靠谱客的博主 潇洒夕阳,最近开发中收集的这篇文章主要介绍SpringSecurity整合Redis实现单点登录及认证返回json数据本文目的依赖登录认证及授权单点登录前后端分离返回json数据其他结尾,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

前一阵刚研究了shiro框架,现在再来研究一下SpringSecurity。
SpringSecurity是由spring团队开发的,为web应用安全性提供了完整的解决方案的框架。

目录

  • 本文目的
  • 依赖
  • 登录认证及授权
    • 认证授权代码:
    • 配置类
  • 单点登录
  • 前后端分离返回json数据
    • 登录成功
    • 登录失败
    • 登出
    • 未认证的请求
    • 访问无权限的请求
    • 配置类
  • 其他
    • 配置无需认证的请求
    • 接口返回结果过滤
    • 定制个性化user类

本文目的

SpringSecurity自身携带登录页面,并且在拦截未认证的请求时默认会重定向至登录页面,但是这种模式不能满足现在的前后端分离的架构,所以我们希望在登录、拦截未认证请求和登出时,向前端返回包含状态编码和消息的json数据。
还有另一个目的就是,生产环境上都是多点部署,更何况如今已是分布式和微服务大行其道,如何做到一次登录,多点访问,也是我们这篇文章需要解决的问题。

依赖

<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>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.76</version>
</dependency>

登录认证及授权

在这里需要实现UserDetailsService接口,并实现loadUserByUsername方法,在这个方法里我们需要根据userName参数查询系统中的用户信息,当用户不存在时抛出UsernameNotFoundException异常,然后将查询出的用户名密码信息和查询出的权限信息保存进user对象中返回,然后SpringSecurity会根据返回的对象判断此次登录是否验证成功,之后user对象会被保存到SpringSecurity上下文中,在其他位置可在上下文中查询到user对象。

认证授权代码:

package com.training.springsecurity.go.component;

import com.training.springsecurity.go.business.entity.RoleInfo;
import com.training.springsecurity.go.business.entity.UserInfo;
import com.training.springsecurity.go.business.service.UserAuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.List;

@Component("userDetailsService")
public class LocalUserDetailService implements UserDetailsService {
    @Autowired
    private UserAuthService userAuthService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        UserInfo userInfo = userAuthService.getUserInfoByName(userName);
        if(userInfo == null){
            throw new UsernameNotFoundException("不是人");
        }
        List<RoleInfo> roleInfoByUserName = userAuthService.getRoleInfoByUserName(userName);
        StringBuilder sb = new StringBuilder();
        for(RoleInfo r : roleInfoByUserName){
            sb.append(r.getRoleName()+",");
        }
        if(sb.length() > 0){
            sb.deleteCharAt(sb.length() - 1);
        }
        String roleNames = sb.toString();
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(roleNames);
        User user = new User(userName, new BCryptPasswordEncoder().encode(userInfo.getPwd()), auths);
        
        return user;
    }
}

配置类

要想让自定义的登录认证及授权的代码生效,我们需要将代码配置起来。
配置类需要继承WebSecurityConfigurerAdapter,接下来我们将反复提及这个配置类,所以接下来统称其为配置类

@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
	@Autowired
    @Qualifier("userDetailsService")
    private UserDetailsService userDetailsService;
    /**
     * 编码工具
     * @return 
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

至此就可以实现基本的本地用户的登录认证及授权功能了。
但只是基本,在测试的过程中,我们输入错误的用户名时,我们理想中应该抛出UsernameNotFoundException,但实际却不是,而是BadCredentialsException,这是为什么呢,我们明明在代码中写了当找不到用户时就抛出UsernameNotFoundException,实际却没有。
通过查看堆栈,跟踪代码,我发现了一段程序如下
在这里插入图片描述
也就是说,当hideUserNotFoundExceptions的值为false的时候,我们就能如愿的抛出我们想要的异常了。通过跟踪代码得知,这段程序所属的类的子类是DaoAuthenticationProvider,所以我们只需要在配置类中,将hideUserNotFoundExceptions的值设置为false即可。
配置类中增加如下代码:

	/**
     * 配置不隐藏用户没找到的异常
     * 配置UserDetailsService实现类逻辑
     * 配置密码编码工具
     * @return
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return daoAuthenticationProvider;
    }

重启,测试,发现还是不对。继续跟踪代码发现认证方法走了两次,其中一次hideUserNotFoundExceptions还是true。
后来通过查找资料和实验发现,认证方法执行两次的原因是配置类中的daoAuthenticationProvider()和configure(AuthenticationManagerBuilder auth)方法重复配置了userDetailsService。
注释掉configure(AuthenticationManagerBuilder auth)配置之后就好了

@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
	@Autowired
    @Qualifier("userDetailsService")
    private UserDetailsService userDetailsService;
    /**
     * 编码工具
     * @return 
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    /*protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }*/
    
	/**
     * 配置不隐藏用户没找到的异常
     * 配置UserDetailsService实现类逻辑
     * 配置密码编码工具
     * @return
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return daoAuthenticationProvider;
    }
}

单点登录

实现单点登录可以有很多方式,我们这里整合的是redis,拜springboot所赐的便利条件,这里我们需要做的事情并不复杂
首先在application.properties文件中配置redis的基本信息:

spring.redis.host=你的redis地址
spring.redis.port=端口
spring.redis.database=2 #选择数据库,我这边是前两个都有用所以我用了第三个

然后再在配置类中增加如下配置:

/**
 * 配置整合redis共享session时使用
 */
@Autowired
private FindByIndexNameSessionRepository sessionRepository;

/**
 * 配置共享session
 * @return
 */
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry(){
    return new SpringSessionBackedSessionRegistry(this.sessionRepository);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
   // 同一个用户只能有一个session,后登录的会挤掉之前登录的
  http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
}

分别以80和81端口启动服务,在80端口登录后,在同一浏览器访问81端口的自定义的请求可以访问成功,然后再打开另一个浏览器(我是一个Chrome一个IE)登录,然后返回之前的浏览器访问自定义的请求显示不允许访问。

前后端分离返回json数据

我们需要分别对登录成功、登录失败、登出、未认证访问请求和访问无权限的请求返回json数据
我们先逐个说明每个部分的定制,然后再统一说明配置类该如何配置

登录成功

实现AuthenticationSuccessHandler接口:

package com.training.springsecurity.go.component;

import com.alibaba.fastjson.JSONObject;
import com.training.springsecurity.go.business.entity.LocalUserDetails;
import com.training.springsecurity.go.business.entity.RestResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
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.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        /*LocalUserDetails principal = (LocalUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();*/

        RestResult restResult = new RestResult(0, "登录成功");
        /*Map<String,String> map = new HashMap<>();
        map.put("name", principal.getName());
        map.put("IDCard", principal.getIDCard());
        restResult.setData(map);*/
        httpServletResponse.setContentType("text/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        writer.print(JSONObject.toJSONString(restResult));
        writer.close();
        writer.flush();
    }
}

(RestResult是自己创建的类,不是框架里的)

登录失败

实现AuthenticationFailureHandler接口:

package com.training.springsecurity.go.component;

import com.alibaba.fastjson.JSONObject;
import com.training.springsecurity.go.business.entity.RestResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
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.io.PrintWriter;

@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        String msg = e.getMessage();
        RestResult restResult = new RestResult(-1, msg);
        httpServletResponse.setContentType("text/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        writer.print(JSONObject.toJSONString(restResult));
        writer.close();
        writer.flush();
    }
}

登出

实现LogoutSuccessHandler接口:

package com.training.springsecurity.go.component;

import com.alibaba.fastjson.JSONObject;
import com.training.springsecurity.go.business.entity.RestResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
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.io.PrintWriter;

@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        RestResult restResult = new RestResult(0,"登出成功");
        httpServletResponse.setContentType("text/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        writer.print(JSONObject.toJSONString(restResult));
        writer.close();
        writer.flush();
    }
}

未认证的请求

实现AuthenticationEntryPoint接口:

package com.training.springsecurity.go.component;

import com.alibaba.fastjson.JSONObject;
import com.training.springsecurity.go.business.entity.RestResult;
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.io.PrintWriter;

@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        RestResult restResult = new RestResult(-1, "请先登录");
        httpServletResponse.setContentType("text/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        writer.print(JSONObject.toJSONString(restResult));
        writer.close();
        writer.flush();
    }
}

访问无权限的请求

实现AccessDeniedHandler接口:

package com.training.springsecurity.go.component;

import com.alibaba.fastjson.JSONObject;
import com.training.springsecurity.go.business.entity.RestResult;
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.io.PrintWriter;

@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        RestResult restResult = new RestResult(-3, "权限不足");
        httpServletResponse.setContentType("text/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        writer.print(JSONObject.toJSONString(restResult));
        writer.close();
        writer.flush();
    }
}

这块还得说一下限定权限的请求方法怎么写,我们可以应用一个SpringSecurity的一个注解来完成权限的限定:

@GetMapping("/hello")
@PreAuthorize("hasAnyRole('ROLE_selector')")
public String hello(){ 
    return "Hello Security";
}

特别说一下这个角色名称的字符串,必须得用**ROLE_**开头,否则不会生效的,授权时使用的角色名称也是如此

配置类

配置类增加如下配置:

/**
 * 未登录时发送请求拦截回调
 */
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;

/**
 * 登录成功回调
 */
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;

/**
 * 登录失败时回调
 */
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

/**
 * 登出成功时回调
 */
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;

/**
 * 403时回调
 */
@Autowired
private AccessDeniedHandler accessDeniedHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    
	//登出配置
    http.logout().logoutSuccessHandler(logoutSuccessHandler).deleteCookies("SESSION").permitAll();
	//认证及权限配置
    http.formLogin()
            .successHandler(authenticationSuccessHandler)
            .failureHandler(authenticationFailureHandler)
            .and().authorizeRequests()
            .anyRequest().authenticated()
            .and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
            .and().csrf().disable(); //关闭跨域防护
    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
    // 同一个用户只能有一个session
    http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
}

测试未登录访问:
在这里插入图片描述
成功

测试登录:
在这里插入图片描述
输入错误的用户名
在这里插入图片描述
成功

登录后测试权限不足的请求
在这里插入图片描述
成功

测试登出:
在这里插入图片描述
成功

其他

配置无需认证的请求

配置类中增加配置:

@Override
public void configure(WebSecurity web) throws Exception {
     web.ignoring().antMatchers("/st/staticRequest");
}

接口返回结果过滤

返回结果中只有小强

@GetMapping("/getUserInfoList")
@PostFilter("filterObject.userName == '小强'")
public List<UserInfo> getUserInfoList(){
    List<UserInfo> list = new ArrayList<>();
    list.add(new UserInfo("小明","123"));
    list.add(new UserInfo("小强","123"));
    list.add(new UserInfo("小红","123"));
    return list;
}

定制个性化user类

之前我们提到过User这个类,在认证授权的方法中最终返回的就是这个类的对象,并存储至上下文中。但是这个类包含关于用户的内容太少了,只有用户名、密码和角色信息,好在我们可以对其扩展。

package com.training.springsecurity.go.business.entity;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

@Data
public class LocalUserDetails extends User {
    private String name;
    private String IDCard;

    public LocalUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities){
        super(username, password, authorities);
    }
}

可以看到我们扩展了两个属性,一个是name一个是IDCard
然后我们改造LocalUserDetailService的登录认证方法loadUserByUsername,使之返回LocalUserDetails的对象,并为扩展属性赋值

LocalUserDetails localUserDetails = new LocalUserDetails(userName, new BCryptPasswordEncoder().encode(userInfo.getPwd()), auths);
localUserDetails.setName("刘老六");
localUserDetails.setIDCard("1234556745678");
return localUserDetails;

然后我们编写一个在上下文中获取LocalUserDetails对象的请求方法:

@GetMapping("/getCurrentUser")
public LocalUserDetails getCurrentUser(){
    LocalUserDetails user = (LocalUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();

    return user;
}

测试之:
在这里插入图片描述
可以看到我们自定义的扩展信息被查出来了。
还有一个细节,就是password显示的是null,但之前的测试我们也看到了,输入的密码并不为空,之所以显示为null,我相信这应该是springsecurity做出的防御措施

最后

以上就是潇洒夕阳为你收集整理的SpringSecurity整合Redis实现单点登录及认证返回json数据本文目的依赖登录认证及授权单点登录前后端分离返回json数据其他结尾的全部内容,希望文章能够帮你解决SpringSecurity整合Redis实现单点登录及认证返回json数据本文目的依赖登录认证及授权单点登录前后端分离返回json数据其他结尾所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(50)

评论列表共有 0 条评论

立即
投稿
返回
顶部