前几天使用springoot做了一个项目,我在项目中负责登录验证,以及角色权限管理,角色其实也很简单,老师、学生、管理员等。想写一写自己的心得以及一些配置中遇到的坑,绝对的干货。话不多说,开干.第一次写博客,希望大家看了,给一些中肯的建议,大三也不容易啊。
如果使用IDEA直接创建springboot项目
如果使用eclipse,直接去这个网站下载后导入即可项目搭建路径
这里只列举几个springboot跟shiro整合的
org.apache.shiro
shiro-spring
1.4.0
org.crazycake
shiro-redis
3.1.0
这里面可能用到一点redis的知识,不会的自己去学习即可
自定义realm类似于我们的数据源,处理从数据库查询出来的账户密码以及权限验证
需要继承AuthorizingRealm类,并实现里面方法
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User userNew =(User) principalCollection.getPrimaryPrincipal();
User user = service.findAllUserAndRoleAndPer(userNew.getUsername());
List stringRole=new ArrayList<>();
List stringPermission=new ArrayList<>();
for(Role role:user.getRoleList()){
if(role!=null){
stringRole.add(role.getName());
for(Permission permission:role.getPermissionList()){
if(permission!=null){
stringPermission.add(permission.getName());
}
}
}
}
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.addStringPermissions(stringPermission);
info.addRoles(stringRole);
return info;
}
//登录验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username=(String) authenticationToken.getPrincipal();
User user = service.findByUsernameBasicInfo(username);
if(user==null||user.getPassword()==null||"".equals(user.getPassword())){
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
}
在登录验证中authenticationToken.getPrincipal(),是获取从前台传递过来的用户名,而 User user = service.findByUsernameBasicInfo(username);我自己写的一个方法,根据用户名获取用户信息,如过存在,则将user、password、getName()传递到SimpleAuthenticationInfo里面即可
在授权方法里User userNew =(User) principalCollection.getPrimaryPrincipal();把刚刚登陆成功的那个对象拿出来查询该对象的权限,因为只有你登录成功才能授权,否则登录都失败了,还谈什么授权。根据用户名把roles跟Permissions全都查询出来,存入集合,以待备用。
自定义的sessionManager,用于当你登陆成功,可以获取一串token字符串,每次前台请求过来的时候,带上这段字符串后台验证,判断是否登陆过,是否有该权限或者角色。一般是将token放入请求头header中,需要继承DefaultWebSessionManager
private static final String AUTHORIZATION="token";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String sessionId= WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if(sessionId!=null){
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}else{
return super.getSessionId(request, response);
}
}
在配置shiro路径的时候,roles[a,b],通常是某个用户同时有a和b角色才可以,但是实际问题一般是只要含有二者之一即可,所以我们需要重写该类,继承AuthorizationFilter
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
//获取当前访问路径所需要的角色集合
String[] rolesArray = (String[]) o;
//没有角色限制,可以直接访问
if (rolesArray == null || rolesArray.length == 0) {
//no roles specified, so nothing to check - allow access.
return true;
}
Set roles = CollectionUtils.asSet(rolesArray);
//当前subject是roles 中的任意一个,则有权限访问
for(String role : roles){
if(subject.hasRole(role)){
return true;
}
}
return false;
}
需要继承UserFilter,重写preHandle方法,因为前台需要有token传递过来,这个请求属于复杂请求,每次发送请求前,首先发送options请求过来"探路",所以我们需要先把放行,还有Access-control-Allow-Origin的值不能设置成* 否则后台报错
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
setHeader(httpRequest,httpResponse);
return true;
}else{
setHeader(httpRequest,httpResponse);
return true;
}
}
private void setHeader(HttpServletRequest request,HttpServletResponse response){
//跨域的header设置
response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "GET,,POST,PUT,DELETE");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
response.setHeader("Content-Type","application/json;charset=UTF-8");
}
@Component
@ConfigurationProperties(prefix = "redis")
@PropertySource("classpath:application.properties")
@Data
public class RedisConfig {
private Integer port;
private String host;
private String password;
private Integer database;
}
@Configuration
public class ShiroConfiguration {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置如果未登录时需要跳转的url
shiroFilterFactoryBean.setLoginUrl("");
//设置登陆成功后跳转的界面或者url,如果是前后端分离,则可以不写该选项
shiroFilterFactoryBean.setSuccessUrl("");
//登录成功后,但是未授权,则调到未授权的url
shiroFilterFactoryBean.setUnauthorizedUrl("");
//将自定义的角色过滤器放到map里面
Map map = new LinkedHashMap<>();
//这里配置好角色过滤器时,拦截路径时的前缀要写成key的值
map.put("customRoles", 自定义的AuthorizationFilter);
//自定义的过滤器解决跨域
map.put("corsFilter", 自定义的跨域过滤器);
//将自定义的角色过滤器放到shiroFilterFactoryBean里面去
shiroFilterFactoryBean.setFilters(map);
!!!!注意这里有个坑:一定要使用LinkedHashMap而不是hashmap,因为hashmap是无序的,
对于路径的映射,一旦匹配到就会立即返回,我们的配置也是有顺序的,可能有的小伙伴使用了hashmap,路径有时匹配成功,有时失败
//拦截器路径,同一拦截,注意要使用linkedHashMap保证过滤器的顺序
Map filterMap=new LinkedHashMap<>();
//key是需要拦截的路径,value采用哪种过滤器,或者那种角色或权限
//跨域拦截
filterMap.put("/**", "corsFilter");
//登出过滤器
filterMap.put("", "logout");
//匿名访问,也就是说无需的登录即可访问
filterMap.put("", "anon");
//需要登录才能访问的
filterMap.put("", "authc");
//有相应角色才能访问的,例如管理员才能访问
filterMap.put("", "customRoles[admin,root]"); //这里对应上面自定义AuthorizationFilter的key
//有相应权限才能访问的,例如有
filterMap.put("", "perms[*]");
//全局拦截,避免遗漏哪些路径,放到最下面,这里也是要求必须使用linkedhashmap的理由
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
//安全管理器
@Bean
public SecurityManager securityManager(RedisCacheManager redisCacheManager){
DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
//设置会话管理
manager.setSessionManager(customSessionManager());
//设置realm
manager.setRealm(customRealm());
//设置缓存
manager.setCacheManager(redisCacheManager);
return manager;
}
//自定义realm
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm=new CustomRealm();
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customRealm;
}
//自定义session管理器
@Bean
public CustomSessionManager customSessionManager(){
CustomSessionManager customSessionManager=new CustomSessionManager();
customSessionManager.setSessionDAO(redisSessionDAO());
return customSessionManager;
}
//密码匹配器
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher=new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
@Autowired
private RedisConfig redisConfig;
//缓存管理
@Bean
public RedisManager redisManager(){
RedisManager redisManager=new RedisManager();
redisManager.setDatabase(redisConfig.getDatabase());
redisManager.setHost(redisConfig.getHost());
redisManager.setPort(redisConfig.getPort());
redisManager.setPassword(redisConfig.getPassword());
return redisManager;
}
//cache管理器
@Bean
public RedisCacheManager redisCacheManager(RedisManager redisManager){
RedisCacheManager redisCacheManager=new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager);
//设置过期时间,单位是秒
redisCacheManager.setExpire(60*30);
return redisCacheManager;
}
//设置sessionDao
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
public Map login(String username,String password){
Map map = new HashMap<>();
Subject subject= SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken(username, password);
try{
subject.login(token);
//获取sessionid,传到前台
Serializable sessionId = subject.getSession().getId();
map.put("message", RbacStatus.SUCCESS_IN.getMessage());
map.put("session_id", sessionId);
map.put("code", RbacStatus.SUCCESS_IN.getCode());
return map;
}catch (Exception e){
map.clear();
e.printStackTrace();
map.put("message", RbacStatus.ERROR_IN.getMessage());
map.put("code", RbacStatus.ERROR_IN.getCode());
return map;
}
}
这里使用redis的原因有两种:
1.在权限校验时,每次需要查询数据库,用户是否拥有该角色或者权限,访问频率较高,而且修改少,正好符合使用redis特点,对于登录来说,访问频率低,不建议使用。
2.可以将前台传递的token放入redis中,类似于分布式session共享,假如项目集群部署时,放入redis,也可以达到共享,唯一的缺点需要自己维护过期时间,可以每次请求后台路径的时候,更新一次时间,或者前台使用js做个定时器也是可以的,emmmm就这样吧。