Spring|SpringSecurity + JWT自定义授权

SpringSecurity+JWT实现自定义授权 最近学习整合SpringSecurity到分布式框架中,看了几天授权这块实在太难理解,要实现传统的RBAC模型授权还是比较复杂,记录一下。
RBAC RBAC通常有5张表,但是我这里用户和角色是一对一关系,习惯把角色直接放在用户表里面,省去了一张表,看起来也很直观。
Spring|SpringSecurity + JWT自定义授权
文章图片

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 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 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; //用户的权限集合private List authorities; //加这个字段为了防止redis序列化转换异常 boolean accountNonExpired; boolean accountNonLocked; boolean credentialsNonExpired; boolean enabled; @Override//重写getAuthorities()返回自定义的权限集合 public Collection 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 = 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,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,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; } } }

测试 【Spring|SpringSecurity + JWT自定义授权】设置admin角色为1 给该角色分配权限
Spring|SpringSecurity + JWT自定义授权
文章图片

登录成功返回用户的相关信息,token被写入cookie
Spring|SpringSecurity + JWT自定义授权
文章图片

Spring|SpringSecurity + JWT自定义授权
文章图片

带上token请求有权限的/user/getById
Spring|SpringSecurity + JWT自定义授权
文章图片

带上token请求没有权限的/user/getAdmin
Spring|SpringSecurity + JWT自定义授权
文章图片

    推荐阅读