SpringSecurity + JWT自定义授权

SpringSecurity+JWT实现自定义授权

最近学习整合SpringSecurity到分布式框架中,看了几天授权这块实在太难理解,要实现传统的RBAC模型授权还是比较复杂,记录一下。

RBAC

RBAC通常有5张表,但是我这里用户和角色是一对一关系,习惯把角色直接放在用户表里面,省去了一张表,看起来也很直观。
在这里插入图片描述

SpringSecurity配置:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity  //里面已经有一个@Configuration注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtTokenFilter jwtTokenFilter;		//校验JWT的拦截器
    @Autowired
    LoginSuccessHandler loginSuccessHandler;	//登录成功后的操作

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /*
         * 校验token是否合法,这个filter会最先进入
         */
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);

        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //使用JWT,关闭session
                .and()
                .httpBasic()
                .and()
                .authorizeRequests()
                .anyRequest().access("@RBACService.hasAccess(request, authentication)")		//所有的url需要认证
                .and()
                .formLogin()
                .successHandler(loginSuccessHandler)//登录成功后的操作
                // .loginPage("/login")				//使用自定义登录页面
                .permitAll();
        http.formLogin();
        http.rememberMe();//开启记住密码功能,会发送给客户端一个cookie
    }
    
    //配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
自定义认证:

SpringSecurity中认证是由UserDetailsService中的loadUserByUsername(String userName)方法校验用户,校验成功会返回一个UserDetails对象,这个UserDetails对象里面就包含了用户的密码权限等信息,其中 Collection<? extends GrantedAuthority> getAuthorities();这个方法就是获取用户的权限集合,我们自己的权限实体类只需要继承GrantedAuthority这个接口,重写getAuthority()方法。getAuthority()方法发挥的是一个字符串,可以理解为权限的别名,我这里为了方便后面判断权限就直接返回权限的url。所以完全自定义关键注意两点:1.自定义的权限实体实现GrantedAuthority接口,重写getAuthority()方法返回权限标识2.自定义用户的实体类实现UserDetails接口,重写getAuthority()方法返回用户的权限集合

权限实体:
public class Access implements GrantedAuthority, Serializable {
    private Integer id;
    private String accessName;
    private String accessUrl;
    private String authority;		//加这个参数为了防止redis序列化转换异常
    private Integer parentId;
    private Integer menuLevel;		//菜单级别0:方法,1:一级菜单,2:二级,3:三级...
    private List<Access> children;
    
    @Override
    public String getAuthority() {	//重写getAuthority()方法返回权限标识
        return this.accessUrl;		//这里返回权限的url方便直接和请求的url进行比对
    }
    
    ......省略一大堆getter/setter
}
用户实体:
public class User implements Serializable, UserDetails {
    private Integer id;
    private String username;
    private String password;
    private String userPhone;
    private Integer roleId;
    private List<Access> access;		//用户的权限集合

    private List<Access> authorities;	//加这个字段为了防止redis序列化转换异常
    boolean accountNonExpired;
    boolean accountNonLocked;
    boolean credentialsNonExpired;
    boolean enabled;
    
    @Override							//重写getAuthorities()返回自定义的权限集合
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return access;	//Access类已经实现了GrantedAuthority接口并重写了getAuthority()方法
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
    
    ......getter/setter......
}

loadUserByUsername()方法执行返回UserDetails才表示认证成功,我这里使用自己的用户实体实现了UserDetails,所以能够直接返回。也可以直接用org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities(accesses).build();构建一个UserDetails返回。后面的密码比对就交给SpringSecurity来完成。注:使用了密码编码器后数据库中的密码应该存储加密后的密码

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper usersMapper;
    @Autowired
    private AccessMapper accessMapper;
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        User user = usersMapper.getByUserName(userName);	//查找用户,这里的username是表单输入的用户名
        if(user == null){
            throw new UsernameNotFoundException("用户不存在");
        }
        List<Access> access = accessMapper.getAccessByUserName(user.getUsername());
        user.setAccess(access);
        //如果嫌实现接口麻烦可以使用这行构建一个
        //UserDetails user= org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities(accesses).build();
        return user;
    }
}

认证成功:

用户登录成功后根据配置的Security会来到自定义的loginSuccessHandler,一般在这里返回给前端一些用户的权限等信息,同时这里使用了JWT的验证方式,登录成功后需要向客户端返回一个token,下次请求服务器时带上这个token,通过token来校验用户身份信息。这里我用了redis来进行存储token,方便控制用户登录。

import com.colaiven.cola_consumer.config.dto.ResponseMsg;
import com.colaiven.cola_consumer.config.dto.ResultMsg;
import com.colaiven.cola_consumer.model.User;
import com.colaiven.cola_consumer.util.JwtUtils;
import com.colaiven.cola_consumer.util.MenuUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    /**  
     * 登录成功执行的操作.
     */
    @Override
    @Transactional
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth){
       try {
           User user = (User) auth.getPrincipal();//auth里面保存了用户的相关信息,getPrincipal()方法会返回一个UserDetails,我这里的User已经实现了UserDetails接口
		   response.setCharacterEncoding("utf-8");	//设置response编码
		   response.setContentType("text/html;charset=utf-8");
		   
           String token = JwtUtils.createJWT("",user.getUsername());//生成token,工具类百度的

           redisTemplate.opsForValue().set(token,user,30,TimeUnit.MINUTES);//将token作为key,user对象作为value存在redis,设置失效时间为30分钟
           user.setPassword(null);	//置空密码。因为要返回用户信息给客户端

           response.setHeader("token",token);	//返回token到客户端
           Cookie cookie = new Cookie("token", token);	//将token写到cookie方便前端获取
           cookie.setPath("/");
           response.addCookie(cookie);
           ResponseMsg msg = new ResponseMsg(ResultMsg.SUCCESS,user);
           PrintWriter writer = response.getWriter();
           writer.write(msg.toString());//返回格式为JSON格式,这里的toString已经设置返回为JSON
           writer.flush();
       }catch (Exception e) {
           e.printStackTrace();
       }
    }
}
授权

当客户端下一次发送请求时,首先依然是认证,因为我使用jwt去校验用户不在使用session,Security不会自动去帮你核验用户,我们只需要讲登录者的信息保存在SecurityContextHolder上下文中,即表示认证成功。根据我的Security配置首先会来到自定义的JwtTokenFilter,所以首先需要拿到客户端请求时携带的token并解析用户信息,这里的用户信息里面包含了权限。如果客户端未携带token则表示用户未登录。这里我是将用户信息存储到redis中,拿到token后去redis获取用户信息,获得用户的真实权限,再根据客户端请求的url与值比对,成功后生成UsernamePasswordAuthenticationToken对象放到Security上下文中继续后面的授权操作。

import com.colaiven.cola_consumer.model.Access;
import com.colaiven.cola_consumer.model.User;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

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.List;

@Component
public class JwtTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader("token");	//获取客户端携带的token
        if(!StringUtils.isEmpty(token)){
            User user = (User) redisTemplate.opsForValue().get(token);      //根据token获取redis中的用户信息
            if(user != null){
                AbstractAuthenticationToken authenticationToken 
                	= new UsernamePasswordAuthenticationToken(user,user.getPassword(),user.getAccess());
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        chain.doFilter(request,response);
    }
}

根据配置接下来会走到自定义的RBACService,在这里能够拿到request和认证成功后的UserDetails。在这里进行自定义权限核验:

import com.colaiven.cola_consumer.model.Access;
import com.colaiven.cola_consumer.model.User;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;

@Component
public class RBACService {
    public boolean hasAccess(HttpServletRequest request, Authentication auth) {
        try{
            User user = (User) auth.getPrincipal();
            String url = request.getRequestURI();
            for (Access access:user.getAccess()) {	//校验权限
                if(url.equals(access.getAccessUrl())){
                    return true;
                }
            }
            return false;
        }catch (Exception e){
            System.out.println("RBACService:"+e.toString());
            return false;
        }
    }
}
测试

设置admin角色为1 给该角色分配权限
在这里插入图片描述
登录成功返回用户的相关信息,token被写入cookie
登录成功返回用户权限
在这里插入图片描述
带上token请求有权限的/user/getById
在这里插入图片描述
带上token请求没有权限的/user/getAdmin
在这里插入图片描述

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值