我所理解的动态权限就是RBAC(Role-Based Access Control)。
就是可以自定义角色,配置角色可以访问哪些URL。然后给不同的角色设置不同的角色。
为什么用Spring Security?听说Spring Security是基于Shiro的。Shiro没用过。之所以用Spring Security是因为它安全。废话!是因为可以帮你防御csrf等攻击。其实现在的Chrome浏览器的同源策略已经很不错了,想csrf也没那么容易。Spring Security能防住多少我也不知道,总之比什么都不做好。XSS的话大家做好过滤就好了。比如Spring MVC就可以过滤掉一些,页面用<c:out/>
标签输出。跑题了啊,其实我也就是想装个X而已。
参考这里 How to create custom methods for use in spring security expression language annotations
一直想用Spring Security实现动态权限管理,这回搞定了,总算了结了一个心愿。
废话不多说,假设你已经会用Spring Security了。如果您不会,请多看官方文档。Spring的文档写的相当到位,就怕你没耐心看。比如你用过Spring MVC,而且已经有一个Spring MVC的项目了。现在你想在你的Spring MVC项目中集成Spring Security怎么办?看这个tutorial:Hello Spring MVC Security Java Config
只想啰嗦一句。之前Spring MVC的配置文件在getServletConfigClasses这个方法里,现在要挪到getRootConfigClasses方法里。至于为什么,文档里说了,下面的注释里也有,注释都来自官方文档。
/**
* it’s even the recommended way to set up your Spring MVC application
*/
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { WebConfig.class };
// The @ComponentScan is loading all configuration within the same package (and child packages) as RootConfiguration. Since SecurityConfig is in this package, it will be loaded with our existing setup and there is nothing more to do.
}
@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
其实很简单。自定义一个类。在类里加一个方法:
@Component("mySecurityService")
public class MySecurityService {
public boolean hasPermission(Authentication authentication, Object foo) {
System.out.println("来了");
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
while(iterator.hasNext()){
GrantedAuthority ga = iterator.next();
if("ROLE_ADMIN".equals(ga.getAuthority())){
return true;
}
}
return false;
}
}
然后在要使用的地方加一个@PreAuthorize("@mySecurityService.hasPermission(authentication, #model)")
就OK了。
@PreAuthorize("@mySecurityService.hasPermission(authentication, #model)")
@RequestMapping(value = { "/testMethod" }, method = RequestMethod.GET)
public String testMethodSecurity(ModelMap model) {
model.addAttribute("greeting", "恭喜你。此页面必须同时拥有admin权限才能访问哦");
return "welcome";
}
注意这里的authentication.getAuthorities();别小看这个,这可是大有来头!
Spring Security总共分为两大块:anthentication和authorization。
刚开始学Spring Security的时候这两个地方你都会卡住!为什么?
Spring Security有一个默认的anthentication。它会帮你生成一个登录用的form表单。蛋疼的是这个form表单只有username和password两个参数。问题来了,我想加个验证码怎么办? 不只这样,默认的authentication会自动帮你注入硬编码的角色。就是你在配置文件里配置的ROLE_USER,ROLE_ADMIN等等。我想要自定义角色,我想要动态角色怎么办?卡住了不是?
如果你使用默认的authentication那么此时authentication.getAuthorities();拿到的角色就是固定的(你在配置文件里配置的)。
authorization你会卡在什么地方?文档里举的例子是@PreAuthorize("hasRole('ADMIN') AND hasRole('DBA')")
看到了吧,这里的ROLE就是固定的,而不是动态的。
所以你需要的是@PreAuthorize(“canIAccess(authorities)”)。即自定义一个decide方法,并且把动态角色传给这个decide方法!
怎么做?
登录(authenticate)的时候想带一个验证码,我们需要自定义一个form表单。 想要动态角色就要在登录成功的时候把用户的角色信息从db中查出来,把它注入到Spring的某个类中,具体哪个类我忘了,文档里有提到,你得仔细看文档“Architecture and Implementation”这一节。 然后在decide方法里拿到之前注入的角色信息。
说起来容易,做起来可没那么容易!
比如说你的验证码参数,在认证(authenticate)的时候如何获取?你可能会说,先把captcha存到session里。只要在认证的时候拿到request对象不就行了吗?关键是你拿的到吗?!
想在认证的时候拿到request太不容易了:
首先得在BeanPostProcessor里注册我们自定义的认证源:MyWebAuthenticationDetailsSource
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String name) throws BeansException {
if (bean instanceof UsernamePasswordAuthenticationFilter) {
MyWebAuthenticationDetailsSource myWebAuthenticationDetailsSource = new MyWebAuthenticationDetailsSource();
((UsernamePasswordAuthenticationFilter) bean)
.setAuthenticationDetailsSource(myWebAuthenticationDetailsSource);
}
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// System.out.println(bean.getClass());
return bean;
}
}
在MyWebAuthenticationDetailsSource的buildDetails方法里返回一个自定义的详细认证类MyWebAuthenticationDetails
public class MyWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new MyWebAuthenticationDetails(context);
}
}
在详细认证类里让Spring自动帮我们注入request对象:
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {
/**
* serialVersionUID
*/
private static final long serialVersionUID = 1435615659672216808L;
private HttpServletRequest request;
public MyWebAuthenticationDetails(HttpServletRequest request) {
super(request);
this.request = request;
}
public HttpServletRequest getRequest() {
return request;
}
public void setRequest(HttpServletRequest request) {
this.request = request;
}
}
最后在我们自定义的认证里拿到MyWebAuthenticationDetails,从而拿到request
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private CustomUserService userService;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MyWebAuthenticationDetails myDetails = (MyWebAuthenticationDetails) authentication.getDetails();
HttpServletRequest request = myDetails.getRequest();
String captcha = request.getParameter("captcha");
HttpSession session = request.getSession();
String realCaptcha = (String) session.getAttribute("captcha");
if(!realCaptcha.equals(captcha)){
throw new BadCredentialsException("验证码错误");
}
String username = authentication.getName();
String password = (String) authentication.getCredentials();
CustomUser user = userService.loadUserByUsername(username);
// http://docs.spring.io/spring-security/site/faq/faq.html#faq-extra-login-fields
if (user == null || !user.getUsername().equalsIgnoreCase(username) || !password.equals(user.getPassword())) {
throw new BadCredentialsException("用户名或密码错误");
}
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
return new UsernamePasswordAuthenticationToken(user, password, authorities);
}
public boolean supports(Class<?> arg0) {
return true;
}
}
注意这个CustomUserService是我们自定义的UserDetailsService。就是通过这个类把用户信息从db里查出来。为了方便我就直接在service里写死了,懒得定义dao,从db里查了。
@Service
public class CustomUserService implements UserDetailsService{
// @Autowired
// private UserDAOImpl userDao;
public CustomUser loadUserByUsername(String username) throws UsernameNotFoundException {
// return userDao.loadUserByUsername(username);
CustomUser user = new CustomUser();
if("bill".equals(username)){
user.setFirstName("kb");
user.setLastName("gc");
user.setUsername("bill");
user.setPassword("ff9830c42660c1dd1942844f8069b74a");
CustomRole userRole = new CustomRole();
userRole.setName("ROLE_USER");
List<CustomRole> roles = new ArrayList<CustomRole>();
roles.add(userRole);
user.setAuthorities(roles);
} else if("admin".equals(username)){
user.setFirstName("kb2");
user.setLastName("gc2");
user.setUsername("admin");
user.setPassword("ff9830c42660c1dd1942844f8069b74a");
List<CustomRole> roles = new ArrayList<CustomRole>();
CustomRole userRole = new CustomRole();
userRole.setName("ROLE_USER");
roles.add(userRole);
CustomRole adminRole = new CustomRole();
adminRole.setName("ROLE_ADMIN");
roles.add(adminRole);
user.setAuthorities(roles);
} else if("dba".equals(username)){
user.setFirstName("kb3");
user.setLastName("gc3");
user.setUsername("dba");
user.setPassword("ff9830c42660c1dd1942844f8069b74a");
List<CustomRole> roles = new ArrayList<CustomRole>();
CustomRole userRole = new CustomRole();
userRole.setName("ROLE_USER");
roles.add(userRole);
CustomRole adminRole = new CustomRole();
adminRole.setName("ROLE_ADMIN");
roles.add(adminRole);
CustomRole dbaRole = new CustomRole();
dbaRole.setName("ROLE_DBA");
roles.add(dbaRole);
user.setAuthorities(roles);
} else{
user = null;
}
return user;
}
}
CustomRole也是我们自定义的ROLE。这样你可以在自定义的ROLE加其他字段,这里我只定义一个了name字段:
public class CustomRole implements GrantedAuthority{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthority() {
return this.name;
}
}
自定义认证就在CustomAuthenticationProvider的authenticate方法里完成。前面做了那么多工作就为了区区一个request。这里有一句maimapi不知当讲不当讲。
在authenticate方法的最后return new UsernamePasswordAuthenticationToken(user, password, authorities); 就把authorities注入到Spring的某个类里了。 所以在MySecurityService的hasPermission方法里我们可以拿到authentication,进而拿到authorities:
@Component("mySecurityService")
public class MySecurityService {
public boolean hasPermission(Authentication authentication, Object foo) {
System.out.println("来了");
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
while(iterator.hasNext()){
GrantedAuthority ga = iterator.next();
if("ROLE_ADMIN".equals(ga.getAuthority())){
return true;
}
}
return false;
}
}
曲折啊,你说这句maimapi当讲不当讲?