SpringSecurity实现登录和自定义权限认证

Posted ☆叙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity实现登录和自定义权限认证相关的知识,希望对你有一定的参考价值。

介绍

        Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

        Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理。 Spring Security提供有若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。根据自己的需要,可以使用适当的过滤器来保护自己的应用程序。

        本文权限认证不采用springsecurity的注解方式,使用自定义的权限认证,这样不需要在控制层的每个接口前面加上权限注解,具体请看下面的权限校验流程。

登录流程和权限校验流程

具体代码查看实现中的github地址

登录流程

        首先,系统会将输入的用户名和密码放入authentication中,之后进入UserDetailsService的实现类中,调用sql,通过用户名查询账号信息和角色信息,统一存储在UserDetails中,之后PasswordEncoder会将查询到的账号密码和authentication中密码进行比对,密码不一致,则抛出异常(认证失败),密码正确则开始执行登录的业务,使用jwt生成token,将UserDetails存储到redis中,然后将token和角色id集返回给客户端。

权限校验流程

        首先,进入自定义的过滤器中,获取客户端传的token,如果token为空,则进入下一层跳出该过滤器,进入配置中,查询是否为不需要权限认证的接口,不是则抛出异常(认证失败);如果token不为空,则使用jwt解析token,获取其中的userId,根据该userId查询redis中存储的用户信息,查询为空则抛出异常(账户过期,请重新登录),之后获取客户端传的role_id(角色id,因为一个用户可能有多个角色,因此需要传个角色id,告诉服务器当前角色),将客户端传的role_id和之前在redis中查询的用户信息比对,查看是否存在该角色,存在,则通过该role_id查询redis中该角色的权限,判断当前请求路径该角色是否拥有权限,有权限则将权限信息封装到authentication中,放行。

 

实现

具体代码请看github地址https://github.com/cn-g/springsecurity

下面展示关键代码实现

springsecurity配置文件

因为新版本SpringSecurity弃用了WebSecurityConfigurerAdapter,所以新版的SpringSecurity需要更换SpringSecurity的配置文件,下面将新版和老版的springsecurity配置代码各自展示了一份

不继承WebSecurityConfigurerAdapter

import com.example.springsecurity.filter.JwtAuthenticationTokenFilter;
import com.example.springsecurity.filter.AuthenticationEntryPointImpl;
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.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Admin
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig

    @Resource
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Resource
    AuthenticationEntryPointImpl authenticationEntryPoint;


    @Bean
    public PasswordEncoder passwordEncoder()
        return new BCryptPasswordEncoder();
    

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception 
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests()
                //登录接口,允许所有人访问
                .antMatchers("/user/login").permitAll()
                //除了上面的接口,其它接口都需要鉴权认证
                .anyRequest().authenticated();
        //配置登入认证失败、权限认证失败异常处理器
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        //把token校验过滤器添加到过滤链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
        return http.build();
    

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception 
        final List<GlobalAuthenticationConfigurerAdapter> configurers = new ArrayList<>();
        configurers.add(new GlobalAuthenticationConfigurerAdapter() 
                            @Override
                            public void configure(AuthenticationManagerBuilder auth)
                                // auth.doSomething()
                            
                        
        );
        return authConfig.getAuthenticationManager();
    

继承WebSecurityConfigurerAdapter

import com.example.springsecurity.filter.JwtAuthenticationTokenFilter;
import com.example.springsecurity.filter.AuthenticationEntryPointImpl;
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.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @author xu
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter

    @Resource
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Resource
    AuthenticationEntryPointImpl authenticationEntryPoint;

    @Bean
    public PasswordEncoder passwordEncoder()
        return new BCryptPasswordEncoder();
    

   @Override
   protected void configure(HttpSecurity http) throws Exception
       http
               //关闭csrf
               .csrf().disable()
               //不通过Session获取SecurityContext
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               .and()
               .authorizeHttpRequests()
               //登录接口,允许所有人访问
               .antMatchers("/user/login").permitAll()
               //除了上面的接口,其它接口都需要鉴权认证
               .anyRequest().authenticated();
       //配置登入认证失败、权限认证失败异常处理器
       http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
       //把token校验过滤器添加到过滤链中
       http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
       //允许跨域
       http.cors();
   

   @Bean
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception
       return supper.authenticationManagerBean();
   

过滤器实现

/**
 * token校验以及权限校验
 * 
 * @author xu
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter 

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException 
        // 获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) 
            // 放行
            filterChain.doFilter(request, response);
            return;
        
        String userId;
        // 解析token
        try 
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
         catch (Exception e) 
            logger.error("解析token失败");
            WebUtil.renderString(response, JSON.toJSONString(ResponseModels.loginException()));
            return;
        
        // 从redis中获取用户信息
        String redisKey = "login:" + userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (ObjectUtils.isEmpty(loginUser)) 
            logger.error("用户信息获取失败");
            WebUtil.renderString(response, JSON.toJSONString(ResponseModels.commonException("账户过期,请重新登录")));
            return;
        
        String roleId = request.getHeader("role_id");
        if(ObjectUtils.isEmpty(roleId))
            logger.error("无角色id");
            WebUtil.renderString(response, JSON.toJSONString(ResponseModels.loginException()));
            return;
        
        // 校验是否有该角色
        if (!loginUser.getPermissions().contains(roleId)) 
            logger.error("角色不匹配");
            WebUtil.renderString(response, JSON.toJSONString(ResponseModels.noPowerException()));
            return;
        
        String url = request.getRequestURI();
        String role = redisCache.getCacheObject("role:role_" + roleId);
        // 校验角色是否存在该路径
        if (!role.contains(url)) 
            logger.error("该接口路径无权限");
            WebUtil.renderString(response, JSON.toJSONString(ResponseModels.noPowerException()));
            return;
        
        // 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
            new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        // 存入securityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    

UserDetailsServiceImpl实现:

/**
 * UserDetailsService实现类
 * 
 * @author xu
 */
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService 

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String userName) 
        log.info("用户名称为:", userName);
        User user = userMapper
            .selectOne(Wrappers.lambdaQuery(User.class).eq(User::getUsername, userName).eq(User::getStatus, 0));
        if (ObjectUtils.isEmpty(user)) 
            throw new UsernameNotFoundException("用户名不存在");
        
        // 获取当前用户角色信息
        List<String> list = roleMapper.selectRoleByUserId(user.getId());
        log.info("用户的角色为:", list);
        //LoginUser为UserDetails的实现类
        return new LoginUser(user, list);
    

登录、登出业务实现

    public ResponseModelDto login(User user) 
        UsernamePasswordAuthenticationToken authenticationToken =
            new UsernamePasswordAuthenticationToken(user.getUsername(),         user.getPassword());
        Authentication authentication =     authenticationManager.authenticate(authenticationToken);
        if (ObjectUtils.isEmpty(authentication)) 
            throw new CommonException("用户名或密码错误");
        
        log.info("用户登录成功:", authentication);
        // 使用userId生成token
        LoginUser loginUser = (LoginUser)authentication.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        redisCache.setCacheObject("login:" + userId, loginUser);
        // 获取当前用户角色信息
        List<String> list = roleMapper.selectRoleByUserId(Long.valueOf(userId));
        HashMap<String, String> map = new HashMap<>();
        map.put("token", jwt);
        map.put("role", String.join(",",list));
        //返回token和角色id集
        return ResponseModels.ok(map);
    

    @Override
    public ResponseModelDto logout() 
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser)authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        redisCache.deleteObject("login:" + userId);
        return ResponseModels.ok("登出成功");
    

SpringSecurity:认证和自定义登陆界面

目录

配置

解决中文乱码问题

认证

直接认证

使用数据库认证

自定义登录界面

替换默认的登陆界面

关闭csrf防护


配置

配置初始化器

public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer 
    //不用重写任何内容
  	//这里实际上会自动注册一个Filter,SpringSecurity底层就是依靠N个过滤器实现的

创建一个配置类用于配置 SpringSecurity

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 
		//继承WebSecurityConfigurerAdapter,之后会进行配置

在初始化器中把该配置类添加进去

@Override
protected Class<?>[] getRootConfigClasses() 
    return new Class[]RootConfiguration.class, SecurityConfiguration.class;

解决中文乱码问题

在初始化器添加,要在进入过滤链之前设置编码方式

public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer 
    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) 
        servletContext.addFilter("CharacterEncodingFilter", new CharacterEncodingFilter("utf-8", true))
                .addMappingForUrlPatterns(null, false, "/*");
        //super.beforeSpringSecurityFilterChain(servletContext);
    

认证

要实现登录功能,就需要用户认证

直接认证

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception 
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();  
//这里使用SpringSecurity提供的BCryptPasswordEncoder
    auth
            .inMemoryAuthentication() //直接验证方式
            .passwordEncoder(encoder) //密码加密器
            .withUser("test")   //用户名
            .password(encoder.encode("123456"))   //这里需要填写加密后的密码
            .roles("user");   //用户的角色

SpringSecurity 的密码校验并不是直接使用原文进行比较,而是使用加密算法将密码进行加密,而且这个过程是不可逆的,然后将用户提供的密码以同样的方式加密后与密文进行比较,保存也是密文,所以即使数据库被窃取了也无法得知密码是多少,很好地保证了用户信息的安全性。

使用数据库认证

需要创建一个 Service 来实现 接口UserDetailsService,它支持我们自己返回一个 UserDetails 对象,我们只需直接返回一个包含数据库中的用户名、密码等信息的 UserDetails 即可,SpringSecurity 会自动进行比对,在配置类中进行扫描并将其注册为 Bean

@Service
public class UserAuthService implements UserDetailsService 
    @Resource
    UserMapper mapper;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException 
        String password = mapper.getPasswordByUsername(s);  //从数据库根据用户名获取密码
        if(password == null)
            throw new UsernameNotFoundException("登录失败,用户名或密码错误!");
        return User   //这里需要返回UserDetails,SpringSecurity会根据给定的信息进行比对
                .withUsername(s)
                .password(password)   //直接从数据库取的密码
                .roles("user")   //用户角色
                .build();
    

最后再修改一下 Security 配置类

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception 
    auth
      .userDetailsService(service)   //使用自定义的Service实现类进行验证
      .passwordEncoder(new BCryptPasswordEncoder());   //依然使用BCryptPasswordEncoder

自定义登录界面

替换默认的登陆界面

首先来看一下Spring Security的自定义界面,包含了一个重要的东西

    <input

      name="_csrf"

      type="hidden"

      value="83421936-b84b-44e3-be47-58bb2c14571a"

    />

他是隐藏的,为了防止 CSRF 攻击而存在的。

从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法的请求(不仅仅只是登陆请求,这里指的是任何请求路径)进行防护。在不带_csrf的情况下,页面中只要发起了 PATCH,POST,PUT 和 DELETE 请求一定会被拒绝,并返回403错误

需要在请求的时候加入 csrfToken ,也就是"83421936-b84b-44e3-be47-58bb2c14571a",正是 csrfToken,如果提交的是表单类型的数据,那么表单中必须包含此 Token 字符串,键名称为"\\_csrf";如果是 JSON 数据格式发送的,那么就需要在请求头中包含此 Token 字符串。

如果用的是Thymeleaf视图解析器,可以加一个这样的输入框

<input

  type="text"

  th:name="$_csrf.getParameterName()"

  th:value="$_csrf.token"

  hidden

/>

Token 的键名称和 Token 字符串可以通过 Thymeleaf 从 Model 中获取,SpringSecurity 会自动将 Token 信息添加到 Model 中

然后就可以配置自己的登陆界面了

先重写Security 配置类中的另一个configure方法

@Override
protected void configure(HttpSecurity http) throws Exception 
    http
            .authorizeRequests()   //首先需要配置哪些请求会被拦截,哪些请求必须具有什么角色才能访问
            .antMatchers("/static/**").permitAll()    //静态资源,使用permitAll来运行任何人访问(注意一定要放在前面)
            .antMatchers("/**").hasRole("user")     //所有请求必须登陆并且是user角色才可以访问(不包含上面的静态资源)

配置拦截规则,也就是当用户未登录时,哪些路径可以访问,哪些路径不可以访问,如果不可以访问,那么会被自动重定向到登陆页面。

接着需要配置表单登陆和登录页面

.formLogin()       //配置Form表单登陆
.loginPage("/login")       //登陆页面地址(GET)
.loginProcessingUrl("/doLogin")    //form表单提交地址(POST)
.defaultSuccessUrl("/index")    //登陆成功后跳转的页面,也可以通过Handler实现高度自定义
.permitAll()    //登陆页面也需要允许所有人访问

登陆页面需要我们自己去编写 Controller 来实现,登陆请求提交处理由 SpringSecurity 提供,只需要写路径就可以了。

@RequestMapping("/login")
public String login()
    return "login";

再需要配置一下退出登录操作

.and()
.logout()
.logoutUrl("/logout")    //退出登陆的请求地址
.logoutSuccessUrl("/login");    //退出后重定向的地址

注意这里的退出登陆请求也必须是 POST 请求方式(因为开启了 CSFR 防护,需要添加 Token),否则无法访问

<body>
    <form action="logout" method="post">
        <input type="text" th:name="$_csrf.getParameterName()" th:value="$_csrf.token" hidden>
        <button>退出登陆</button>
    </form>
</body>

关闭csrf防护

直接在配置类中配置

.and()

.csrf().disable();

以上是关于SpringSecurity实现登录和自定义权限认证的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security 和自定义外部身份验证

[Java]利用拦截器和自定义注解做登录以及权限验证

五SpringSecurity Web权限方案——自定义登录页面与权限访问控制

五SpringSecurity Web权限方案——自定义登录页面与权限访问控制

[SpringSecurity]web权限方案_用户认证_自定义用户登录页面

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统