有很长一段时间都觉得自己添加个filter,基于RBAC模型,就能很轻松的实现权限控制,没必要引入shiro,spring-security这样的框架增加系统的复杂度。事实上也的确这样,如果你的需求仅仅是控制用户能否访问某个url,使用框架和自己实现filter效果基本一致,区别在于使用shiro和spring-security能够提供更多的扩展,集成了很多实用的功能,整体结构更加规范。
shiro和spring-security有哪些更多功能,这里不再展开,感兴趣的同学可以自行百度,我们这里以shiro为例,讲述spring-boot项目如何整合shiro实现权限控制。
1、添加maven依赖
<!--shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 整合ehcache,减少数据库查询次数 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
2、添加shiro配置
创建ShiroConfigration.java
@Configuration
public class ShiroConfigration {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfigration.class);
private static Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
@Bean
public SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setMaxAge(7 * 24 * 60 * 60);//保存10天
return simpleCookie;
}
/**
* cookie管理对象;
*/
@Bean
public CookieRememberMeManager rememberMeManager() {
logger.debug("ShiroConfiguration.rememberMeManager()");
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey(Base64.decode("kPv59vyqzj00x11LXJZTjJ2UHW48jzHN"));
return cookieRememberMeManager;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy("shiroFilter");
// 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
proxy.setTargetFilterLifecycle(true);
filterRegistration.setFilter(proxy);
filterRegistration.setEnabled(true);
//filterRegistration.addUrlPatterns("/*");// 可以自己灵活的定义很多,避免一些根本不需要被Shiro处理的请求被包含进来
return filterRegistration;
}
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
@Bean(name="securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myShiroRealm());
manager.setRememberMeManager(rememberMeManager());
manager.setCacheManager(ehCacheManager());
return manager;
}
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
* <p>
* Filter Chain定义说明
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean() {
logger.debug("ShiroConfigration.getShiroFilterFactoryBean()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager());
HashMap<String, javax.servlet.Filter> loginFilter = new HashMap<>();
loginFilter.put("loginFilter", new LoginFilter());
shiroFilterFactoryBean.setFilters(loginFilter);
filterChainDefinitionMap.put("/login/submit", "anon");
filterChainDefinitionMap.put("/logout", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/test/**", "anon");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
//配置记住我或认证通过可以访问的地址
filterChainDefinitionMap.put("/", "user");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
filterChainDefinitionMap.put("/**", "loginFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* shiro缓存管理器;
* 需要注入对应的其它的实体类中:
* 1、安全管理器:securityManager
* 可见securityManager是整个shiro的核心;
*
* @return
*/
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return cacheManager;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
shiroFilter是配置的重点,
- anon表示允许匿名访问
- shiroFilterFactoryBean.setFilters(loginFilter)来设置自定义的过滤器,如本处设置了LoginFilter用于添加登录拦截
- filterChainDefinitionMap.put("/**", "loginFilter");用于指定loginFilter的作用范围
3、添加自定义realm
创建类MyShiroRealm.java
public class MyShiroRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
@Autowired
private UserService userService;
@Autowired
private UserRoleService userRoleService;
@Autowired
private RoleService roleService;
@Autowired
private RolePermissionService rolePermissionService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号.
String idObj = (String) token.getPrincipal();
Integer id = NumberUtils.toInt(idObj);
User user = userService.findById(id);
if (user == null) {
// 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到unauthorizedUrl指定的地址
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getId(),
user.getPwd(), getName());
return authenticationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/*
* 当没有使用缓存的时候,不断刷新页面的话,这个代码会不断执行,
* 当其实没有必要每次都重新设置权限信息,所以我们需要放到缓存中进行管理;
* 当放到缓存中时,这样的话,doGetAuthorizationInfo就只会执行一次了,
* 缓存过期之后会再次执行。
*/
logger.debug("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRole("ACTUATOR");
Integer userId = Integer.parseInt(principals.getPrimaryPrincipal().toString());
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
Set<Integer> roleIds = userRoleService.findRoleIds(userId);
Set<Role> roles = roleService.findByIds(roleIds);
for(Role role : roles){
authorizationInfo.addRole(role.getCode());
}
//设置权限信息.
List<Permission> permissions = rolePermissionService.getPermissions(roleIds);
Set<String> set = new HashSet<String>(permissions.size()*2);
for(Permission permission : permissions){
if(StringUtils.isNotBlank(permission.getCode())){
set.add(permission.getCode());
}
}
authorizationInfo.setStringPermissions(set);
return authorizationInfo;
}
}
- doGetAuthenticationInfo用于验证用户账号信息,可根据具体业务来调整认证策略
- doGetAuthorizationInfo用于获取用户拥有的角色和权限
4、创建登录拦截器
public class LoginFilter implements Filter {
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
AjaxResponseWriter.write(req, res, ServiceStatusEnum.UNLOGIN, "请登录");
return;
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
}
public class AjaxResponseWriter {
/**
* 写回数据到前端
* @param request
* @param response
* @param status {@link ServiceStatusEnum}
* @param message 返回的描述信息
* @throws IOException
*/
public static void write(HttpServletRequest request,HttpServletResponse response,ServiceStatusEnum status,String message) throws IOException{
String contentType = "application/json";
response.setContentType(contentType);
response.setCharacterEncoding("UTF-8");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
Map<String, String> map = Maps.newLinkedHashMap();
map.put("code", status.code);
map.put("msg", message);
String result = JacksonHelper.toJson(map);
PrintWriter out = response.getWriter();
try{
out.print(result);
out.flush();
} finally {
out.close();
}
}
}
/**
* 全局性状态码
* @author yangwk
*/
public enum ServiceStatusEnum {
UNLOGIN("0001"), //未登录
ILLEGAL_TOKEN("0002"),//非法的token
;
public String code;
private ServiceStatusEnum(String code){
this.code = code;
}
}
- 用户登录状态拦截器,不允许匿名访问的url会经过该filter,如果未登录,则返回未登录提示(未登录处理可根据具体业务进行调整)
5、添加登录、退出功能
@Api(value="用户登录",tags={"用户登录"})
@RestController
public class LoginController {
private static Logger logger = LoggerFactory.getLogger(LoginController.class);
@Value("${server.session.timeout}")
private String serverSessionTimeout;
/**
* 用户登录接口 通过用户名和密码进行登录
*/
@ApiOperation(value = "用户登录接口 通过用户名和密码进行登录", notes = "用户登录接口 通过用户名和密码进行登录")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "query", name = "username", value = "用户名", required = true, dataType = "String"),
@ApiImplicitParam(paramType = "query", name = "pwd", value = "密码", required = true, dataType = "String"),
@ApiImplicitParam(paramType = "query", name = "autoLogin", value = "自动登录", required = true, dataType = "boolean")})
@RequestMapping(value = "/login/submit",method={RequestMethod.GET,RequestMethod.POST})
public Map<String, String> subm(HttpServletRequest request,HttpServletResponse response,
String username,String pwd,@RequestParam(value = "autoLogin", defaultValue = "false") boolean autoLogin) {
Map<String, String> map = Maps.newLinkedHashMap();
Subject currentUser = SecurityUtils.getSubject();
User user = userService.findByUsername(username);
if (user == null) {
map.put("code", "-1");
map.put("description", "账号不存在");
return map;
}
if (user.getEnable() == 0) { //账号被禁用
map.put("code", "-1");
map.put("description", "账号已被禁用");
return map;
}
String salt = user.getSalt();
UsernamePasswordToken token = null;
Integer userId = user.getId();
token = new UsernamePasswordToken(userId.toString(),SaltMD5Util.encode(pwd, salt));
token.setRememberMe(autoLogin);
loginValid(map, currentUser, token);
// 验证是否登录成功
if (currentUser.isAuthenticated()) {
map.put("code","1");
map.put("description", "ok");
map.put("id", String.valueOf(userId));
map.put("username", user.getUsername());
map.put("name", user.getName());
map.put("compnay_id", String.valueOf(user.getCompanyId()));
String uuidToken = UUID.randomUUID().toString();
map.put("token", uuidToken);
currentUser.getSession().setTimeout(NumberUtils.toLong(serverSessionTimeout, 1800)*1000);
request.getSession().setAttribute("token",uuidToken );
} else {
map.put("code", "-1");
token.clear();
}
return map;
}
@RequestMapping(value="logout",method=RequestMethod.GET)
public Map<String, String> logout() {
Map<String, String> map = Maps.newLinkedHashMap();
Subject currentUser = SecurityUtils.getSubject();
currentUser.logout();
map.put("code", "logout");
return map;
}
@RequestMapping(value="unauth",method=RequestMethod.GET)
public Map<String, String> unauth() {
Map<String, String> map = Maps.newLinkedHashMap();
map.put("code", "403");
map.put("msg", "你没有访问权限");
return map;
}
private boolean loginValid(Map<String, String> map,Subject currentUser, UsernamePasswordToken token) {
String username = null;
if (token != null) {
username = (String) token.getPrincipal();
}
try {
// 在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
// 每个Realm都能在必要时对提交的AuthenticationTokens作出反应
// 所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
currentUser.login(token);
return true;
} catch (UnknownAccountException | IncorrectCredentialsException ex) {
map.put("description", "账号或密码错误");
} catch (LockedAccountException lae) {
map.put("description","账户已锁定");
} catch (ExcessiveAttemptsException eae) {
map.put("description", "错误次数过多");
} catch (AuthenticationException ae) {
// 通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
map.put("description", "登录失败");
logger.warn(String.format("对用户[%s]进行登录验证..验证未通过", username),ae);
}
return false;
}
@Autowired
private UserService userService;
}
- 以上代码是比较通用的登录、退出功能,如果没有特殊需求,可直接使用上述功能
6、在接口上添加权限限制
以UserController为例:
@ApiOperation(value="获取用户详细信息", notes="根据ID查找用户")
@ApiImplicitParam(paramType="query",name = "id", value = "用户ID", required = true,dataType="int")
@RequiresPermissions(value={"user:get"})
@RequestMapping(value="/get",method=RequestMethod.GET)
public User get(int id){
User entity = userService.findById(id);
entity.setPwd(null);
entity.setSalt(null);
return entity;
}
@ApiOperation(value="修改密码", notes="修改密码")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "query", name = "oldPwd", value = "旧密码", required = true, dataType = "String"),
@ApiImplicitParam(paramType = "query", name = "pwd", value = "新密码", required = true, dataType = "String"),
@ApiImplicitParam(paramType = "query", name = "confirmPwd", value = "新密码(确认)", required = true, dataType = "String")})
@RequiresPermissions(value={"user:reset-pwd"})
@RequestMapping(value="/reset-pwd",method=RequestMethod.POST)
public Return resetPwd(String oldPwd,String pwd,String confirmPwd){
if(StringUtils.isBlank(oldPwd) || StringUtils.isBlank(pwd)
|| StringUtils.isBlank(confirmPwd) || !pwd.equals(confirmPwd)) {
return Return.fail("非法参数");
}
Subject currentUser = SecurityUtils.getSubject();
Integer userId=(Integer) currentUser.getPrincipal();
User entity = userService.findById(userId);
if(!entity.getPwd().equals(SaltMD5Util.encode(oldPwd, entity.getSalt()))){
return Return.fail("原始密码错误");
}
return userService.changePwd(entity,pwd);
}
- @RequiresPermissions 和 @RequiresRoles分别用于限制该方法可访问的权限和角色,两者如果同时使用,默认是“&”关系;两者的value参数都可以设置为数组,数组元素间的关系可以通过logical属性来设置,有Logical.AND,Logical.OR两个值可选择
小结
spring-boot整合shiro的步骤如下:
- 添加maven依赖
- 添加ShiroConfigration配置,指定shiro的核心配置
- 添加MyShiroRealm,指定账户认证策略和角色权限获取方式
- 添加LoginFilter,即登录拦截器
- 添加登录、退出功能
- 通过注解添加接口调用权限限制
权限控制基于RBAC模型,涉及的表有:用户(user)、角色(role)、用户角色关系(user_role)、权限(permission)、角色权限关系(role_permission),具体代码可参考github内的示例项目。
本人搭建好的spring boot web后端开发框架已上传至GitHub,欢迎吐槽!
https://github.com/q7322068/rest-base,已用于多个正式项目,当前可能因为版本问题不是很完善,后续持续优化,希望你能有所收获!