shiro整合springmvc

说明

  代码及部分相关资料根据慕课网Mark老师的视频进行整理

  其他资料:

  • shiro官网

流程

配置

1) 配置web.xml整合shiro
把shiro整合到springMVC实质上是在web.xml配置过滤器(filter),配置DelegatingFilterProxy,让其代理shiro的过滤器,对需要认证或者授权的请求路径进行过滤。



    shiroFilter
    org.springframework.web.filter.DelegatingFilterProxy


    shiroFilter
    /*

2) 配置spring.xml添加shiro组件
与上一节的程序一样,需要添加SecutiryManager、Realm两个核心组件。

  • (非必需)创建HashedCredentialsMatcher。用于加密,也可不加密,根据自己需求进行配置,建议加密。


    
    
  • 创建realm。此处示例使用自定义的可加密MyEncryptedRealm,引用HashedCredentialMatcher


    
  • 创建SecurityManager。示例使用DefaultWebSecurityManager,引用上面的realm。

    
  • 创建ShiroFilterFactoryBean。该Bean会根据配置,生成一个被DelegatingFilterProxy代理的,类型为SpringShiroFilter的过滤器,这个过滤器包含FilterChain,用于对请求进行实际上的更详细的过滤。该Bean的id必须与web.xml中配置的DelegatingFilterProxy的“filter-name”一致。


    
    
    
    
    
        
            /login.html = anon
            /subLogin = anon
            /register = anon
            /addPermissions = anon
            /* = authc
        
    

filterChain从上到下匹配,当匹配到合适的规则时进行处理,不管后面的规则如何,所以一定要注意顺序。 value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值。
anon:它对应的过滤器里面是空的,什么都没做;
authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
shiro包含11个过滤器,具体信息可查看shiro官网

实战1

maven依赖

    
    
        org.springframework
        spring-context
        4.3.5.RELEASE
    
    
        org.springframework
        spring-webmvc
        4.3.5.RELEASE
    
    
    
        org.apache.shiro
        shiro-core
        1.4.0
    
    
        org.apache.shiro
        shiro-spring
        1.4.0
    
    
        org.apache.shiro
        shiro-web
        1.4.0
    
    
    
        org.slf4j
        slf4j-log4j12
        1.7.26
    
    
        log4j
        log4j
        1.2.17
    

工程结构

shiro整合springmvc_第1张图片

配置文件

web.xml:

   

   

   
   
       contextConfigLocation
       
       classpath*:spring/spring*
   

   
   
       org.springframework.web.context.ContextLoaderListener
   
   

   
   
       shiroFilter
       org.springframework.web.filter.DelegatingFilterProxy
   
   
       shiroFilter
       /*
   

   
   
       spring-mvc
       org.springframework.web.servlet.DispatcherServlet
       
           contextConfigLocation
           classpath*:spring/spring-mvc.xml
       
       1
   
   
       spring-mvc
       /
   
   

spring.xml:

    
    

    
    
        
        
    

    
    
        
    

    
    
        
    

    
    
        
        
        
        
        
            
                /login.html = anon
                /subLogin = anon
                /register = anon
                /addPermissions = anon
                /* = authc
            
        
    
    

springmvc.xml:

    
    

    
    

    
    

    
    
    

log4j.properties:

# Global logging configuration #\u5728\u5f00\u53d1\u73af\u5883\u4e0b\u65e5\u5fd7\u7ea7\u522b\u8981\u8bbe\u7f6e\u6210DEBUG\uff0c\u751f\u4ea7\u73af\u5883\u8bbe\u7f6e\u6210info\u6216error
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

后端代码

com.lifeofcoding.shiro.pojo.User.java

package com.lifeofcoding.shiro.pojo;

import java.util.Set;

public class User {
    
    private String username;
    private String password;
    private Set
    
      roles;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Set
     
       getRoles() {
        return roles;
    }

    public void setRoles(Set
      
        roles) {
        this.roles = roles;
    }
}

      
     
    
com.lifeofcoding.shiro.realm.MyEncryptedRealm.java

package com.lifeofcoding.shiro.realm;

import com.lifeofcoding.shiro.pojo.User;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class MyEncryptedRealm extends AuthorizingRealm {

    /** 加密次数 */
    private int iterations;
    /** 算法名 */
    private String algorithmName;

    /** 存储用户名和密码 */
    private final Map
    
      userMap;
    /** 存储用户及其对应的角色 */
    private final Map
     
      > roleMap;
    /** 存储所有角色以及角色对应的权限 */
    private final Map
      
       
        > permissionMap;
    /** 存储用户盐值 */
    private Map
        
          saltMap; { //设置Realm名,可用于获取该realm super.setName("MyRealm"); } public MyEncryptedRealm(){ this.iterations = 0; this.algorithmName = "MD5"; this.userMap = new ConcurrentHashMap<>(16); this.roleMap = new ConcurrentHashMap<>(16); this.permissionMap = new ConcurrentHashMap<>(16); this.saltMap = new ConcurrentHashMap<>(16); } /** * 身份认证必须实现的方法 * @param authenticationToken token to do authenticate * @return org.apache.shiro.authc.AuthenticationInfo */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //1.获取主体中的用户名 String userName = (String) authenticationToken.getPrincipal(); //2.通过用户名获取密码,getPasswordByName自定义实现 String password = getPasswordByUserName(userName); if(null == password){ return null; } //3.构建authenticationInfo认证信息 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm"); //添加盐值 String salt = getSaltByUsername(userName); authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt)); return authenticationInfo; } /** * 用于授权,必须实现 * @param principalCollection principals * @return org.apache.shiro.authz.AuthorizationInfo */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //1.获取用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。 String userName = (String) principalCollection.getPrimaryPrincipal(); //2.获取角色信息,getRoleByUserName自定义 Set
         
           roles = getRolesByUserName(userName); //3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息 Set
          
            permissions = getPermissionsByUserName(userName); //4.构建认证信息并返回。 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setStringPermissions(permissions); simpleAuthorizationInfo.setRoles(roles); return simpleAuthorizationInfo; } /** * 往realm添加账号信息 * @param user user */ public void addAccount(User user) throws UserExistException { String userName = user.getUsername(); String password = user.getPassword(); Set
           
             roles = user.getRoles(); if(null != userMap.get(userName)){ throw new UserExistException("user \""+ userName +"\" already exist"); } //如果设置的加密次数大于0,则对密码进行加密 if(iterations > 0){ //此处用随机数作为盐值,可改为UUID或其它 String salt = String.valueOf(Math.random()*10); saltMap.put(userName,salt); password = doHash(password, salt); } userMap.put(userName, password); //如果roles不为空,存入roleMap if (!CollectionUtils.isEmpty(roles)){ roleMap.put(userName, roles); } } /** * 自定义部分,通过用户名获取权限信息 * @param userName username * @return java.util.Set
            
              */ private Set
             
               getPermissionsByUserName(String userName) { //1.先通过用户名获取角色信息 Set
              
                roles = getRolesByUserName(userName); if (CollectionUtils.isEmpty(roles)){ return null; } //2.通过角色信息获取对应的权限 Set
               
                 permissions = new HashSet<>(); roles.forEach(role -> { if (null != permissionMap.get(role)) { permissions.addAll(permissionMap.get(role)); } }); return permissions; } /** * 自定义部分,通过用户名获取密码,可改为数据库操作 * @param userName username * @return java.lang.String */ private String getPasswordByUserName(String userName){ return userMap.get(userName); } /** * 自定义部分,通过用户名获取角色信息,可改为数据库操作 * @param userName username * @return java.util.Set
                
                  */ private Set
                 
                   getRolesByUserName(String userName){ return roleMap.get(userName); } /** * 自定义部分,通过用户名获取角色信息,可改为数据库操作 * @param userName username * @return java.util.Set
                  
                    */ private String getSaltByUsername(String userName) { return saltMap.get(userName); } /** * 往realm删除账号信息 * @param userName username */ public void deleteAccount(String userName){ userMap.remove(userName); roleMap.remove(userName); } /** * 添加角色权限,变参不传值会接收到长度为0的数组。 * @param roleName name of the role * @param permissions permissions which this role preserve */ public void addPermissions(String roleName,Set
                   
                     permissions){ permissionMap.put(roleName, permissions); } /** * 设置加密次数 * @param iterations iterations to doHash */ public void setHashIterations(int iterations){ this.iterations = iterations; } /** * 设置算法名 * @param algorithmName name of the algorithm to use */ public void setAlgorithmName(String algorithmName){ this.algorithmName = algorithmName; } /** * 计算哈希值 * @param str str to doHash * @param salt user's salt */ private String doHash(String str,String salt){ salt = null==salt ? "" : salt; return new SimpleHash(this.algorithmName,str,salt,this.iterations).toString(); } /** * 注册时,用户已存在的异常 */ public class UserExistException extends Exception{ private UserExistException(String message) {super(message);} } } 
                   
                  
                 
                
               
              
             
            
           
          
         
        
       
      
     
    
com.lifeofcoding.shiro.controller.UserController.java

package com.lifeofcoding.shiro.controller;

import com.lifeofcoding.shiro.pojo.User;
import com.lifeofcoding.shiro.realm.MyEncryptedRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {

    @Autowired
    private MyEncryptedRealm realm;

    /**
     * 用户登录
     * @param user 用户信息,包括用户名(username)和密码(password)
     * api示例: POST /subLogin?username=java&password=123
     * */
    @ResponseBody
    @RequestMapping(value = "/subLogin",method = RequestMethod.POST,produces= {"application/json;charset=UTF-8"})
    public String subLogin(User user){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
        try {
            subject.login(token);
        }catch (Exception e){
            return e.getMessage();
        }
        return "\""+subject.getPrincipal().toString()+"\""+"登陆成功";
    }

    /**
     * 用户注册
     * @param user 用户信息,包括:用户名(username)、密码(password)、角色(roles)(可选)
     * @return 返回注册信息
     * api示例: POST /register?username=java&password=123&roles=admin&roles=user  可通过指定多个roles传入roles数组
     * */
    @ResponseBody
    @RequestMapping(value = "/register",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})
    public String register(User user){
        //配置realm设置加密方式
        realm.setAlgorithmName("MD5");
        //加密次数
        realm.setHashIterations(3);
        //添加账号
        try {
            realm.addAccount(user);
        }catch (Exception e){
            return e.getMessage();
        }
        return "Add account \"" + user.getUsername() + "\" succeeded";
    }

    /**
     * 测试已登录的用户是否拥有某角色
     * @param role 角色名
     * @return 返回信息
     * api示例: GET /testRole?role=admin
     * */
    @ResponseBody
    @RequestMapping(value = "testRole",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String testRole(String role){
        if (null == role){
            return "no input";
        }
        Subject subject = SecurityUtils.getSubject();
        if (subject.hasRole(role)){
            return "user \"" + subject.getPrincipal()+"\" has role \"" + role +"\"";
        }
        return  "user \"" + subject.getPrincipal()+"\" do not have role \"" + role + "\"";
    }

    /**
     * 测试已登录的用户是否拥有某权限
     * @param permission 权限
     * @return 返回信息
     * api示例: GET /testPermission?permission=user:delete
     * */
    @ResponseBody
    @RequestMapping(value = "testPermission",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String testPermission(String permission){
        if (null == permission){
            return "no input";
        }
        Subject subject = SecurityUtils.getSubject();
        if (subject.isPermitted(permission)){
            return "user \"" + subject.getPrincipal()+"\" has permission \"" + permission +"\"";
        }
        return  "user \"" + subject.getPrincipal()+"\" do not have permission \"" + permission + "\"";
    }

    /**
     * 添加权限
     * api示例: GET /addPermissions?role=admin&permissions=user:delete&permissions=user:modify
     * */
    @ResponseBody
    @RequestMapping(value = "addPermissions",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String addPermissions(String role, String...permissions){
        if (role==null || CollectionUtils.isEmpty(CollectionUtils.asSet(permissions))){
            return "rolename or permissions can not be empty";
        }
        realm.addPermissions(role, CollectionUtils.asSet(permissions));
        return null;
    }
}

前端代码

login.html:




    
    Title



用户名: \
密码: \

实战2——自定义jdbcRealm

代码与实战1基本一致,仅仅是修改Realm,改为从数据库中获取信息,再修改相关配置。

maven依赖

    
    
    
        org.apache.shiro
        shiro-web
        1.4.0
    
    
        org.apache.shiro
        shiro-spring
        1.4.0
    
    
        org.apache.shiro
        shiro-spring
        1.4.0
    
    
    
        org.springframework
        spring-webmvc
        4.3.5.RELEASE
    
    
        org.springframework
        spring-context
        4.3.5.RELEASE
    
    
    
        log4j
        log4j
        1.2.17
    
    
        org.slf4j
        slf4j-log4j12
        1.7.26
    
    
    
        mysql
        mysql-connector-java
        8.0.15
    
    
        com.alibaba
        druid
        1.1.6
    
    
        org.springframework
        spring-jdbc
        4.3.5.RELEASE
    
    
    
        org.aspectj
        aspectjweaver
        1.8.10
    
    

项目结构

shiro整合springmvc_第2张图片

配置文件

spring.xml:

    
    

    
    
        
        
    

    
    
        
    

    
    
        
    

    
    
        
        
        
        
        
            
                /login.html = anon
                /subLogin = anon
                /register = anon
                /addPermissions = anon
                /testPermission = anon
                /testRole = anon
                /* = authc
            
        
    
    

spring-dao.xml:

    
    

    
    
        
        
        
    

    
    
        
    

    
    
        
    

    
    
    
        
            
        
    

    
    
        
        
    
    

spring-mvc.xml:

    
    

    
    

    
    

    
    
    

log4j.properties:

\# Global logging configuration \#\u5728\u5f00\u53d1\u73af\u5883\u4e0b\u65e5\u5fd7\u7ea7\u522b\u8981\u8bbe\u7f6e\u6210DEBUG\uff0c\u751f\u4ea7\u73af\u5883\u8bbe\u7f6e\u6210info\u6216error
log4j.rootLogger=DEBUG, stdout
\# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

后台代码

UserDaoImpl.java


package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.UserDao;
import com.lifeofcoding.shiro.pojo.User;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Component
public class UserDaoImpl implements UserDao {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public String getPasswordByUserName(String userName) {
        String queryPasswordSql = "SELECT password FROM shiro_web_users WHERE username = ?";
        List
    
      passwords = jdbcTemplate.query(queryPasswordSql, new String[]{userName}, new RowMapper
     
      () {
            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("password");
            }
        });
        if(CollectionUtils.isEmpty(passwords)){
            return null;
        }
        return passwords.get(0);
    }

    @Override
    public Set
      
        getRolesByUserName(String userName) {
        String queryRoleSql = "SELECT role FROM shiro_web_user_roles WHERE username = ?";
        List
       
         roles = jdbcTemplate.query(queryRoleSql, new String[]{userName}, new RowMapper
        
         () { @Override public String mapRow(ResultSet resultSet, int i) throws SQLException { return resultSet.getString("role"); } }); if (CollectionUtils.isEmpty(roles)){ return null; } return new HashSet<>(roles); } @Override public String getSaltByUserName(String userName) { String querySaltSql = "SELECT salt FROM shiro_web_users WHERE username = ?"; List
         
           salts = jdbcTemplate.query(querySaltSql,new String[]{userName},new RowMapper
          
           () { @Override public String mapRow(ResultSet resultSet, int i) throws SQLException { return resultSet.getString("salt"); } }); if (CollectionUtils.isEmpty(salts)){ return null; } return salts.get(0); } @Override public void addUser(User user) throws Exception{ if (user == null){ return; } String addUserSql = "INSERT INTO shiro_web_users (username,password,salt) VALUES (?,?,?)"; jdbcTemplate.update(addUserSql,new Object[]{user.getUsername(),user.getPassword(),user.getSalt()}); } @Override public void deleteUser(String userName) { String deleteUserSql = "DELETE FROM shiro_web_users WHERE username = ?"; jdbcTemplate.update(deleteUserSql,userName); } } 
          
         
        
       
      
     
    
PermissionDaoImpl.java
package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.PermissionDao;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Component
public class PermissionDaoImpl implements PermissionDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public void addPermissions(String roleName, Set
    
      permissions) {
        String addPermissionSql = "INSERT IGNORE INTO shiro_web_roles_permissions (role,permission) VALUES (?,?)";
        //去掉空数据
        permissions.remove("");
        //后面StatementSetter需要用index遍历集合,所以转为List
        ArrayList
     
       tempPermissions = new ArrayList<>(permissions);
        //批量添加数据
        jdbcTemplate.batchUpdate(addPermissionSql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, roleName);
                ps.setString(2, tempPermissions.get(i));
            }

            @Override
            public int getBatchSize() {
                return tempPermissions.size();
            }
        });
    }

    @Override
    public Set
      
        getPermissionsByRole(String role) {
        String queryPermissionSql = "SELECT permission FROM shiro_web_roles_permissions WHERE role = ?";
        List
       
         permissions = jdbcTemplate.query(queryPermissionSql, new String[]{role}, new RowMapper
        
         () {
            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("permission");
            }
        });
        if (CollectionUtils.isEmpty(permissions)){
            return null;
        }
        return new HashSet<>(permissions);
    }

    @Override
    public void deletePermissionsByRole(String role) {
        String deletePermissionsByRoleSql = "DELETE FROM shiro_web_roles_permissions WHERE role = ?";
        jdbcTemplate.update(deletePermissionsByRoleSql,role);
    }

    @Override
    public void deletePermission(String permission) {
        String deletePermissionSql = "DELETE FROM shiro_web_roles_permissions WHERE permission = ?";
        jdbcTemplate.update(deletePermissionSql,permission);
    }

    @Override
    public void deleteRolePermission(String role, String permission) {
        String deleteRolePermissionSql = "DELETE FROM shiro_web_roles_permissions WHERE role = ? AND permission = ?";
        jdbcTemplate.update(deleteRolePermissionSql,new Object[]{role,permission});
    }
}

        
       
      
     
    
RoleDaoImpl.java
package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.RoleDao;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Set;

@Component
public class RoleDaoImpl implements RoleDao {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public void addRole(String username, Set
    
      roles) {
        //去掉空数据
        roles.remove("");
        String addRoleSql = "INSERT IGNORE INTO shiro_web_user_roles (username,role) VALUES (?,?)";
        //StatementSetter用index遍历集合,转为List
        ArrayList
     
       tempRoles = new ArrayList<>(roles);
        //批量添加数据
        jdbcTemplate.batchUpdate(addRoleSql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, username);
                ps.setString(2, tempRoles.get(i));
            }

            @Override
            public int getBatchSize() {
                return tempRoles.size();
            }
        });
    }

    @Override
    public void deleteRolesByUsername(String userName) {
        String deleteRoleByUsernameSql = "DELETE FROM shiro_web_user_roles WHERE username = ?";
        jdbcTemplate.update(deleteRoleByUsernameSql,userName);
    }

    @Override
    public void deleteRole(String role) {
        String deleteRoleSql = "DELETE FROM shiro_web_user_roles WHERE role = ?";
        jdbcTemplate.update(deleteRoleSql,role);
    }

    @Override
    public void deleteUserRole(String userName, String role) {
        String deleteUserRoleSql = "DELETE FROM shiro_web_user_roles WHERE username = ? AND role = ?";
        jdbcTemplate.update(deleteUserRoleSql,new Object[]{userName,role});
    }
}

     
    
MyEncryptedJdbcRealm.java
package com.lifeofcoding.shiro.realm;

import com.lifeofcoding.shiro.dao.PermissionDao;
import com.lifeofcoding.shiro.dao.RoleDao;
import com.lifeofcoding.shiro.dao.UserDao;
import com.lifeofcoding.shiro.pojo.User;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;

public class MyEncryptedJdbcRealm extends AuthorizingRealm {
    @Resource
    private UserDao userDao;
    @Resource
    private PermissionDao permissionDao;
    @Resource
    private RoleDao roleDao;

    /**加密次数*/
    private int iterations;
    /**加密算法名*/
    private String algorithmName;

    /*---------------------------------实现自定义Realm需要重写的两个方法------------------------------------*/

    /**
     * 身份认证必须实现的方法
     * @param authenticationToken token
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取主体中的用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
        String userName = (String) authenticationToken.getPrincipal();
        //2.通过用户名获取密码,getPasswordByName自定义实现
        String password = getPasswordByUserName(userName);
        if(null == password){
            return null;
        }
        //3.如果密码不为空,则构建authenticationInfo认证信息
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
        String salt = getSaltByUserName(userName);
        //4.认证信息添加盐值
        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
        return authenticationInfo;
    }

    /**
     * 用于授权,必须实现
     * @param principalCollection principal的集合
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.获取用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.获取角色信息,getRoleByUserName自定义
        Set
    
      roles = getRolesByUserName(userName);
        //3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息
        Set
     
       permissions = getPermissionsByUserName(userName);
        //4.构建认证信息并返回。
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //添加权限信息
        simpleAuthorizationInfo.setStringPermissions(permissions);
        //添加角色信息
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    //类加载时初始化
    {
        //设置Realm名,可用于获取该realm
        super.setName("MyJdbcRealm");
    }

    /**构造方法,初始化哈希次数及算法名称*/
    MyEncryptedJdbcRealm(){
        iterations = 0;
        algorithmName = "MD5";
    }

    /*--------------------------------------自定义部分--------------------------*/

    /**
     * 自定义部分,通过用户名获取权限信息
     * @param userName username
     * @return 该用户拥有的所有权限
     */
    public Set
      
        getPermissionsByUserName(String userName) {
        //1.先通过用户名获取所有角色信息
        Set
       
         roles = userDao.getRolesByUserName(userName);
        //2.通过角色信息获取对应的权限
        Set
        
          permissions = new HashSet<>();
        roles.forEach(role -> {
            Set
         
           tempPermissions = permissionDao.getPermissionsByRole(role); if (null != tempPermissions) { permissions.addAll(tempPermissions); } }); return permissions; } /** * 自定义部分,通过用户名获取密码 * @param userName username * @return java.lang.String */ public String getPasswordByUserName(String userName){ return userDao.getPasswordByUserName(userName); } /** * 自定义部分,通过用户名获取盐 * @param userName username * @return java.lang.String */ public String getSaltByUserName(String userName){ return userDao.getSaltByUserName(userName); } /** * 自定义部分,通过用户名获取角色信息 * @param userName username * @return java.util.Set
          
            */ public Set
           
             getRolesByUserName(String userName){ return userDao.getRolesByUserName(userName); } /** * 往realm添加账号信息 * @param user user */ public void addAccount(User user) throws Exception { String salt = ""; String password = user.getPassword(); String userName = user.getUsername(); //用户信息为空抛出异常 if (user.getUsername()==null || user.getPassword()==null){ throw new InfoEmptyException("username or password can not be empty"); } //如果用户已经注册,抛出异常 if(null != userDao.getPasswordByUserName(userName)){ throw new UserExistException("user \""+ userName +"\" already exist"); } //如果设置的加密次数大于0,则进行加密 if(iterations > 0){ salt = randomSalt(); password = doHash(password, salt); } user.setPassword(password); user.setSalt(salt); userDao.addUser(user); if (CollectionUtils.isEmpty(user.getRoles())){ return; } roleDao.addRole(userName,user.getRoles()); } /** * 添加角色权限 * @param roleName 角色名 * @param permissions 该角色拥有的权限 */ public void addPermissions(String roleName, Set
            
              permissions) throws Exception{ permissionDao.addPermissions(roleName,permissions); } /** * 用随机数作为盐值,可改为UUID或其他 * */ public String randomSalt(){ return String.valueOf(Math.random()*10); } /** * 删除账号信息 * @param userName 用户名 */ public void deleteAccount(String userName) throws Exception{ userDao.deleteUser(userName); roleDao.deleteRolesByUsername(userName); } /** * 设置加密次数 * @param iterations 哈希操作的次数 */ public void setHashIterations(int iterations){ this.iterations = iterations; } /** * 设置算法名 * @param algorithmName 哈希算法名 */ public void setAlgorithmName(String algorithmName){ this.algorithmName = algorithmName; } /** * 进行哈希运算 * @param source 原来的字符 * @param salt 盐值 * @return 运算结果 * */ private String doHash(String source, String salt){ return new SimpleHash(this.algorithmName,source,salt,this.iterations).toString(); } /** * 注册时,用户已存在的异常类 */ public class UserExistException extends Exception{ public UserExistException(String message) {super(message);} } /** * 用户信息为空的异常 * */ public class InfoEmptyException extends Exception{ public InfoEmptyException(String message) {super(message);} } } 
            
           
          
         
        
       
      
     
    

实战3——通过注解授权

配置

在springmvc配置文件中添加如下配置,务必在springmvc配置文件中添加,即上面的springmvc.xml文件。









    
  

后端代码

直接在controller上添加注解"@RequiresRoles"或者"@RequiresPermissions",如:

@RequiresPermissions("user:delete")
@RequiresRoles("admin")
@ResponseBody
@RequestMapping(value = "testRole",method = RequestMethod.GET)
public String testRole(){
    return "has role: admin";
}

使用拥有指定角色或者权限的用户登录,即可访问到该"testRole()"方法,否则会抛异常。
也可以用数组传多个参数进行授权,如:

@RequiresPermissions({"user:delete","user:login"})
@RequiresRoles({"user","admin"})

当当前用户同时拥有所有指定的角色或者权限时,才能访问方法。

实战4——redis实现session管理

实现session管理,主要是给SecurityManager配置SessionManager,而SessionManager,需要配置用于Session增删查改的SessionDao。SessionDao继承AbstractSessionDAO抽象类,需要实现的方法有:

  • Serializable doCreate(Session session)
    存储session
  • Session doReadSession(Serializable sessionId)
    读取session
  • void update(Session session) throws UnknownSessionException
    更新session
  • void delete(Session session)
    删除session
  • Collection getActiveSessions()
    获取活跃的session

maven

添加redis依赖



    redis.clients
    jedis
    3.0.0

后台代码

封装jedis的增删查改操作:

JedisUtil.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import java.util.HashSet;
import java.util.Set;

@Component
public class JedisUtil {
/**jedis连接池*/
@Autowired
private JedisPool jedisPool;

/**获取资源*/
private Jedis getResource(){
    return jedisPool.getResource();
}

/**
 * set
 * */
public byte[] set(byte[] key, byte[] value) {
    Jedis jedis = getResource();
    try{
        jedis.set(key, value);
        return value;
    }finally {
        jedis.close();
    }
}

/**
 * 设置过期时间
 * */
public void expire(byte[] key, int seconds) {
    Jedis jedis = getResource();
    try {
        jedis.expire(key,seconds);
    } finally {
        jedis.close();
    }
}

/**
 * 获取值
 * */
public byte[] get(byte[] key) {
    Jedis jedis = getResource();
    try {
        return jedis.get(key);
    } finally {
        jedis.close();
    }
}

/**
 * 删除
 * */
public void del(byte[] key) {
    Jedis jedis = getResource();
    try {
        jedis.del(key);
    } finally {
        jedis.close();
    }
}

/**
 * "keys"操作
 * */
public Set keys(String pattern) {
    Jedis jedis = getResource();
    try {
        return jedis.keys((pattern).getBytes());
    } finally {
        jedis.close();
    }
}

/**
 * 使用scan获取所有匹配的keys,redis2.8+开始,加入了"scan"操作,
 * 允许每次只获取一部分数据,避免数据量大时"keys"造成阻塞
 * */
public Set scan(String pattern){
    Jedis jedis = getResource();
    //初始化游标
    byte[] START_CURSOR = "0".getBytes();
    //每次要求返回的数据量
    int NUM_PER_SCAN = 50;
    try{
        //设置初始化游标
        byte[] cursor = START_CURSOR;
        //查询参数对象
        ScanParams params = new ScanParams();
        //设置匹配模式
        params.match(pattern.getBytes());
        //设置理想的每次返回的数据数量(不一定会返回这么多)
        params.count(NUM_PER_SCAN);
        //用一个HashSet来存储查找到的keys,因为结果可能会重复,所以用set去重
        Set keys = new HashSet<>();
        while(true) {
            /*redis的scan与单循环链表相似,每次scan操作,返回部分数据result以及下次scan操作需要的游标cursor*/
            ScanResult result = jedis.scan(cursor,params);
            //获取下次scan的游标,byte[]类型,如果是String类型,返回结果也会是String类型,需要注意。
            cursor = result.getCursorAsBytes();
            keys.addAll(result.getResult());
            //如果已经遍历完所有数据,则退出
            if(result.isCompleteIteration()) {break;}
        }
        return keys;
    }finally {
        jedis.close();
    }
}

}

AbstractSessionDAO的子类:

RedisSessionDao.java

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.util.CollectionUtils;
import org.springframework.util.SerializationUtils;
import com.lifeofcoding.utils.JedisUtil;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class RedisSessionDao extends AbstractSessionDAO {

/**封装的redis工具类*/
@Resource
private JedisUtil jedisUtil;

/**在redis中存储的session的前缀*/
private final String SHIRO_SESSION_PREFIX="shiro-session:";

/**
 * 把传入的key(sessionId)转化为在redis中存储的统一格式的key
 * */
private byte[] getKey(String key){
    return (SHIRO_SESSION_PREFIX+key).getBytes();
}

/**
 * 保存session到redis中
 * */
private void saveSession(Session session){
    if (null != session && null != session.getId()) {
        //获取session的id并将其传化为指定格式
        byte[] key = getKey(session.getId().toString());
        //对session进行序列化
        byte[] value = SerializationUtils.serialize(session);
        jedisUtil.set(key, value);
        jedisUtil.expire(key, 600);
    }
}

/**
 * 把session保存到redis
 * */
@Override
protected Serializable doCreate(Session session) {
    //创建sessionId
    Serializable sessionId = generateSessionId(session);
    //给session绑定sessionId
    assignSessionId(session,sessionId);
    //保存session到redis中
    saveSession(session);
    return sessionId;
}

/**
 * 读取session
 * */
@Override
protected Session doReadSession(Serializable sessionId) {
    if (null == sessionId) {
        return null;
    }
    //把sessionId转化为redis中的key的格式
    byte[] key = getKey(sessionId.toString());
    byte[] value = jedisUtil.get(key);
    //返回反序列化后的session
    return (Session) SerializationUtils.deserialize(value);
}

/**
 * 更新session
 * */
@Override
public void update(Session session) throws UnknownSessionException {
    saveSession(session);
}

/**
 * 删除session
 * */
@Override
public void delete(Session session) {
    if (null == session && null == session.getId()){
        return;
    }
    byte[] key = getKey(session.getId().toString());
    jedisUtil.del(key);
}

/**
 * 获取活跃的session
 * */
@Override
public Collection getActiveSessions() {
    //获取redis中存储session的所有key
    //Set keys = jedisUtil.keys(SHIRO_SESSION_PREFIX+"*");
    //可以自己改写、优化scan,用"scan"操作替代"keys",避免数据量大时阻塞。
    Set keys = jedisUtil.scan(SHIRO_SESSION_PREFIX+"*");
    Set sessions = new HashSet();
    if (CollectionUtils.isEmpty(keys)){
        return sessions;
    }
    for (byte[] key : keys){
        Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key));
        sessions.add(session);
    }
    return sessions;
}

}

配置文件

redis的配置文件:

spring-redis.xml









    
    
    
    
    



实现了AbstractSessionDao抽象类后,在spring配置文件中配置该实现类,然后配置SessionManager。






    

然后在securityManager中配置sessionManager


    
    
    

SessionManager优化

shiro整合springmvc_第3张图片
  使用DefaultSessionManager管理session时,session通过retrieveSession(SessionKey sessionKey)方法获取,该方法又调用retrieveSessionFromDataSource(sessionId),利用SessionDao从数据源中获取session,此处sessionDao就是之前的自己实现的RedisSessionDao,而“数据源”,就是redis。
  通过debug可以发现有时候在处理一次请求时,retrieveSession方法调用了很多次,这样就意味着访问了很多次redis,这给redis带来了不必要的压力。此时,可以重写该方法,把session存储到request中,需要获取session时,直接从request中获取,避免redis服务器不必要的开销。
  自定义SessionManager,需要继承 DefaultSessionManager的子类DefaultWebSessionManager,而不是直接继承DefaultSessionManager,否则获取到的sessionId和request为null;
代码如下:

CustomSessionManager.java

package com.lifeofcoding.session;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import javax.servlet.ServletRequest;
import java.io.Serializable;

public class CustomSessionManager extends DefaultWebSessionManager {

@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException{
    //通过SessionKey获取SessionId
    Serializable sessionId = getSessionId(sessionKey);
    ServletRequest request = null;
    //通过SessionKey获取ServletRequest
    if (sessionKey instanceof WebSessionKey) {
        request = ((WebSessionKey) sessionKey).getServletRequest();
    }
    //尝试从request中根据sessionId获取session
    if (null!=request && null!=sessionId){
        Session session = (Session) request.getAttribute(sessionId.toString());
        if (null!=session) {
            return session;
        }
    }
    /*如果request中没有session,则使用父类获取session,并保存到request中,
     父类DefaultWebSession是通过SessionDao获取session,在这里是从redis获取*/
    Session session = super.retrieveSession(sessionKey);
    if (null != request && null != sessionId){
        request.setAttribute(sessionId.toString(),session);
    }
    return session;
}

}

自定义SessionManager后,修改配置文件,把DefaultSessionManager改为自己的SessionManager。

 

    

实战5——使用redis实现缓存管理

  在程序中,对用户权限数据的访问量是比较大的,如果每次授权,都去数据库中取数据,这是十分不理想的,可以用redis来充当缓存,缓存用户的授权数据,减轻数据库压力。

后端代码

1.继承Cache类,编写RedisCache,用于对redis中的缓存数据进行增删查改。Cache类实质上相当于DAO,仅仅是对缓存进行增删查改。

RedisCache.java

package com.lifeofcoding.shiro.cache;

import com.lifeofcoding.shiro.utils.JedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.stereotype.Component;
import org.springframework.util.SerializationUtils;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Set;

@Component
public class RedisCache implements Cache {

@Resource
private JedisUtil jedisUtil;

/**
 * cache的前缀
 * */
private final String CACHE_PREFIX = "shiro-cache:";

private byte[] getKey(K k){
    if (k instanceof String){
        return (CACHE_PREFIX + k).getBytes();
    }
    return SerializationUtils.serialize(k);
}

@Override
public V get(K k) throws CacheException {
    System.out.println("read cache from redis for user: "+k.toString());
    byte[] value = jedisUtil.get(getKey(k));
    if (null != value){
        return (V) SerializationUtils.deserialize(value);
    }
    return null;
}

@Override
public V put(K k, V v) throws CacheException {
    byte[] key = getKey(k);
    byte[] value = SerializationUtils.serialize(v);
    jedisUtil.set(key,value);
    jedisUtil.expire(key,600);
    return v;
}

@Override
public V remove(K k) throws CacheException {
    byte[] key = getKey(k);
    byte[] value = jedisUtil.get(key);
    jedisUtil.del(key);
    if (null != value){
        return (V) SerializationUtils.deserialize(value);
    }
    return null;
}

@Override
public void clear() throws CacheException {

}

@Override
public int size() {
    return 0;
}

@Override
public Set keys() {
    return null;
}

@Override
public Collection values() {
    return null;
}

}

2.继承CacheManager,编写RedisCacheManager,用来返回cache。CacheManager只有一个方法“getCache(String var1)”,通过传入cache的名字,返回对应的cache,仅此而已。

RedisCacheManager.java

package com.lifeofcoding.shiro.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import javax.annotation.Resource;

public class RedisCacheManager implements CacheManager {
@Resource
private RedisCache redisCache;

/**
 * 该方法用来给shiro获取cache对象。
 * 参数s为cache的名称,此处只有一个cache,即RedisCache,直接返回单例的RedisCache实例即可。
 * */
@Override
public  Cache getCache(String s) throws CacheException {
    return redisCache;
}

}

<

配置文件

给SecurityManager配置CacheManager






    
    
    

拓展

把授权数据放redis,每次需要授权数据时就访问redis,这对redis的资源也造成一定浪费,可以在RedisCache中用Map等集合类,构造二级缓存,每次需要数据,直接从二级缓存中获取,如果没有数据,再从redis中取。

实战6——RememberMe

很多情况下,网站需要提供“记住我”的功能,可以使用shiro的CookieRememberMeManager实现。在配置方面只需在spring配置文件中添加配置即可。



    
    




    




    
    
    
    

当然,也要修改User类,添加rememberMe字段,让用户自行决定是否启用该功能,同时修改UserController实现该功能。

private boolean rememberMe;

public boolean getRememberMe() {
    return rememberMe;
}

public void setRememberMe(boolean rememberMe) {
    this.rememberMe = rememberMe;
}
@ResponseBody
@RequestMapping(value = "/subLogin",method = RequestMethod.POST,produces= {"application/json;charset=UTF-8"})
public String subLogin(User user){
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
    try {
        //设置自动登录
        token.setRememberMe(user.getRememberMe());
        subject.login(token);
    }catch (Exception e){
        return e.getMessage();
    }
    return "\""+subject.getPrincipal().toString()+"\""+"登陆成功";
}

文件传送门

github地址

你可能感兴趣的:(shiro整合springmvc)