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地址icon-default.png?t=M85Bhttps://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("登出成功");
    }
  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

☆叙

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

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

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

打赏作者

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

抵扣说明:

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

余额充值