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