1.依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2.数据库三张表:用户角色权限
-- auto-generated definition
create table t_user
(
id int auto_increment
comment '用户主键'
primary key,
username varchar(20) not null
comment '用户名',
password varchar(20) not null
comment '密码',
role_id int null
comment '关联role表'
)
-- auto-generated definition
create table t_role
(
id int auto_increment
comment '主键'
primary key,
rolename varchar(20) null
comment '角色名称'
)
charset = utf8;
-- auto-generated definition
create table t_permission
(
id int auto_increment
comment '主键'
primary key,
permissionname varchar(50) not null
comment '权限名,如/user/teacher',
role_id int null
comment '关联role'
)
charset = utf8;
3.自定义ream
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取用户会话对象,即登录认证的
User user = (User) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//根据用户名或id从数据库/redis读取用户角色与权限进行权限校验
authorizationInfo.setRoles(userMapper.getRoles(user.getUserName()));
authorizationInfo.setStringPermissions(userMapper.getPermissions(user.getUserName()));
return authorizationInfo;
}
//登录认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String userName = (String) authenticationToken.getPrincipal();
User user = userMapper.getByUsername(userName);
//传入要本地session或redis存储的会话对象user,第二个为要与登录入参密码比对的user表用户密码
return user==null?null:new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
}
}
4.几个测试接口
@RestController
@RequestMapping("/user")
public class HomeController {
@PostMapping("/login")
public String login(@RequestBody User user, HttpServletRequest request) {
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword(),user.isRememberMe());
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token); // 开始认证,这一步会跳到我们自定义的 Realm 中
return "success";
}catch(Exception e){
e.printStackTrace();
request.setAttribute("error", "用户名或密码错误!");
return "login";
}
}
/**
* 身份认证测试接口
* @param request
* @return
*/
@RequestMapping("/admin")
public String admin(HttpServletRequest request) {
return "success";
}
/**
* 角色认证测试接口
* @param request
* @return
*/
@RequiresRoles(value = "student")
@RequestMapping("/student")
public String student(HttpServletRequest request) {
return "success";
}
/**
* 权限认证测试接口
* @param request
* @return
*/
@RequiresPermissions(value = "/user/teacher")
@RequestMapping("/teacher")
public String teacher(HttpServletRequest request) {
return "success";
}
}
5.shiro核心配置
@Configuration
@Slf4j
public class ShiroConfig {
//shiro过滤器配置
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置默认登录的 URL,身份认证失败会访问该 URL
shiroFilterFactoryBean.setLoginUrl("/user/login");
// 设置成功之后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/success");
// 设置未授权界面,权限认证失败会访问该URL
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
// LinkedHashMap 是有序的,进行顺序拦截器配置
Map<String,String> filterChainMap = new LinkedHashMap<>();
// 配置可以匿名访问的地址,可以根据实际情况自己添加,放行一些静态资源等,anon 表示放行
filterChainMap.put("/css/**", "anon");
filterChainMap.put("/imgs/**", "anon");
filterChainMap.put("/js/**", "anon");
filterChainMap.put("/swagger-*/**", "anon");
filterChainMap.put("/swagger-ui.html/**", "anon");
filterChainMap.put("/login", "anon");
// 具有“admin”角色才允许,一般在接口使用@RequiresRoles配置
//filterChainMap.put("/user/student*/**", "roles[admin]");
// 具有“user:create”权限才允许一般在接口使用@RequiresPermissions配置
// filterChainMap.put("/user/teacher*/**", "perms[\"user:create\"]");
// 配置 logout 过滤器
filterChainMap.put("/logout", "logout");
//没匹配上前面过滤器链中任何过滤器,最后都走authc过滤器,即必须登录
filterChainMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
//shiro核心bean
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(customRealm());
defaultSecurityManager.setRememberMeManager(getCookieRememberMeManager());
//集群部署时需要自定义redis保存会话信息
//defaultSecurityManager.setCacheManager();
//defaultSecurityManager.setSessionManager();
return defaultSecurityManager;
}
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
//首先在登录页面选中RememberMe然后/login接口登录成功,浏览器会把名为RememberMe的Cookie写到客户端并保存下来,重新打开浏览器还能正常调用各接口不用重新登录;是否热rememberMe有用户登录传入参数,并在登录接口中UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword(),user.isRememberMe());进行设置;
如下两个bean配置rememberMe功能的cookie,不配置默认有效期1d
@Bean
public CookieRememberMeManager getCookieRememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(getSimpleCookie());
cookieRememberMeManager.setCipherKey(Base64.decode("Z3VucwAAAAAAAAAAAAAAAA=="));
return cookieRememberMeManager;
}
@Bean
public SimpleCookie getSimpleCookie() {
SimpleCookie cookie = new SimpleCookie("rememberMe");
cookie.setHttpOnly(true);
cookie.setMaxAge(7 * 24 * 60 * 60);//7天
return cookie;
}
//开启权限shiro注解 @RequiresRoles @RequiresPermissions,aop实现的
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
public class RequestUtils {
/**
* 获取当前登录的用户,若用户未登录,则返回未登录 json
* @return
*/
public static SysUser currentLoginUser() {
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
Object principal = subject.getPrincipals().getPrimaryPrincipal();
if (principal instanceof SysUser) {
return (SysUser) principal;
}
}
return null;
}
}
测试注意:shiro依赖cookie-session机制实现的,需要使用浏览器进行测试,postman等工具不保存jsessionid,会导致每次访问不能根据jsessionid找到上次请求的session信息。
使用感触:感觉还是挺麻烦,单机系统前后端不分离如一些后台管理系统等可能比较适合,分布式集群系统不如直接jwt保存用户session(当然集群部署时shiro是可以结合redis实现session共享的),自定义注解+aop+redis实现权限控制灵活,易于理解。