您的位置:首页 > 编程语言 > Java开发

[置顶] spring-boot实战:shiro

2017-10-02 15:43 483 查看
有很长一段时间都觉得自己添加个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() {

4000
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);
i
9f0e
f(!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的步骤如下:

1. 添加maven依赖

2. 添加ShiroConfigration配置,指定shiro的核心配置

3. 添加MyShiroRealm,指定账户认证策略和角色权限获取方式

4. 添加LoginFilter,即登录拦截器

5. 添加登录、退出功能

6. 通过注解添加接口调用权限限制

权限控制基于RBAC模型,涉及的表有:用户(user)、角色(role)、用户角色关系(user_role)、权限(permission)、角色权限关系(role_permission),具体代码可参考github内的示例项目。

本人搭建好的spring boot web后端开发框架已上传至GitHub,欢迎吐槽!

https://github.com/q7322068/rest-base,已用于多个正式项目,当前可能因为版本问题不是很完善,后续持续优化,希望你能有所收获!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: