天天看點

Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01

這個小項目包含了注冊與登入,使用了springboot+mybatis+shiro的技術棧;當使用者在浏覽器登入時發起請求時,首先這一系列的請求會被攔截器進行攔截(ShiroFilter),然後攔截器根據使用者名去資料庫尋找是否有相對應的user實體;如果有則傳回封裝到User類中(沒有就使用者名錯誤),然後比對密碼是否一緻;如果都通過了則認證成功;登入到首頁面;然後首頁面有不同的功能,不同的使用者擁有不同的權限,有的能看到,有的則無法看到;然後如果不登陸直接通路首頁面,會被強制跳轉到登入頁面;另外登入之後也可以登出;

通過分析上文:​​​​

1.首先快速建立一個springboot項目(使用spring向導)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hao</groupId>
    <artifactId>springboot-shiro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-shiro</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <!--########################################################-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.7.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
    </dependencies>
    <!--########################################################-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>      

2.資料庫建表

Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01

這裡的salt字段主要是為了使用md5+salt+散列加密時,需要儲存salt字元串,以便在業務層比較密碼正确與否時一緻

3.注冊、登入和首頁面

<%@page contentType="text/html; utf-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>使用者注冊</h1>
    <form action="${pageContext.request.contextPath}/user/register" method="post">
        username:<input type="text" name="username"><br>
        password:<input type="password" name="password"><br>
        <input type="submit" value="注冊">
    </form>
</body>
</html>      
<%@page contentType="text/html; utf-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>使用者登入</h1>
    <form action="${pageContext.request.contextPath}/user/login" method="post">
        username:<input type="text" name="username"><br>
        password:<input type="password" name="password"><br>
        <input type="submit" value="登入">
    </form>
</body>
</html>      
<%@page contentType="text/html; utf-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>系統首頁,歡迎你的到來</h1>
    <a href="${pageContext.request.contextPath}/user/outLogin">退出</a>

    <ul>
        <shiro:hasAnyRoles name="user">
            <li><a href="">使用者管理</a>
                <ul>
                    <shiro:hasPermission name="user:add:*">
                        <li><a href="">添加使用者</a></li>
                    </shiro:hasPermission>
                    <li><a href="">删除使用者</a></li>
                    <li><a href="">修改使用者</a></li>
                    <li><a href="">查詢使用者</a></li>
                </ul>
            </li>
        </shiro:hasAnyRoles>
        <shiro:hasRole name="admin">
            <li><a href="">商品管理</a> </li>
            <li><a href="">訂單管理</a> </li>
            <li><a href="">物流管理</a> </li>
        </shiro:hasRole>
        <shiro:hasRole name="shper">
            <li><a href="">終極格式化</a> </li>
        </shiro:hasRole>
    </ul>
</body>
</html>      

4.配置檔案

spring.application.name=shiro
server.servlet.context-path=/shiro

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=hao20001010

mybatis.type-aliases-package=com.hao.springboot.entity
mybatis.mapper-locations=classpath:mapper/*.xml

logging.level.com.hao.springboot.dao=debug      

5.編寫mapper.xml映射檔案

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.hao.springboot.dao.UserDao">
    <insert id="save" parameterType="User">
        insert into t_user values(#{id},#{username},#{password},#{salt})
    </insert>

    <select id="findByUserName" resultType="User">
        select * from t_user where username=#{username}
    </select>
</mapper>      

6.DAO層

@Repository
public interface UserDao {

    void save(User user);

    User findByUserName(String username);
}      

7.service層

public interface UserService {
    void register(User user);

    User findByUserName(String username);
}      
@Service("userService")
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    UserDao userDao;

    @Override
    public void register(User user) {
        //明文密碼進行md5+salt+随機散列
        //1.生成随機鹽
        String salt = SaltUtil.getSalt(8);
        //2.将随機鹽儲存到資料庫中
        user.setSalt(salt);
        Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
        user.setPassword(md5Hash.toHex());
        userDao.save(user);
    }

    @Override
    public User findByUserName(String username) {
        return userDao.findByUserName(username);
    }
}      

8.controller層

/**
 * @author:抱着魚睡覺的喵喵
 * @date:2020/12/29
 * @description:
 */
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;
    /**
     * 處理身份認證
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/login")
    public String login(@RequestParam("username")String username,@RequestParam("password")String password){

        //擷取主體對象
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username,password));
            return "redirect:/index.jsp";
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("使用者名錯誤");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密碼錯誤");
        }
        return "redirect:/login.jsp";
    }

    /**
     * 退出使用者
     * @return
     */
    @RequestMapping("/outLogin")
    public String outLogin(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();           //退出使用者
        return "redirect:/login.jsp";
    }

    /**
     * 使用者注冊
     * @return
     */
    @RequestMapping("/register")
    public String register(User user){
        try {
            userService.register(user);
            return "redirect:/login.jsp";
        }catch (Exception e){
            e.printStackTrace();
            return "redirect:/register.jsp";
        }
    }
}      

9.User實體類

@Data
@ToString
@AllArgsConstructor
@Accessors(chain = true)
@NoArgsConstructor
public class User {

    private String id;
    private String username;
    private String password;
    private String salt;

}      

10.建立ShiroFilter攔截器(攔截所有​

import com.hao.springboot.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * @author:抱着魚睡覺的喵喵
 * @date:2020/12/29
 * @description:
 */
@Configuration
public class ShiroConfig {
    /**
     * 1.建立ShiroFilter
     * 負責攔截所有請求
     * @return shiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactory(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //給filter設定安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //配置系統首先資源
        //配置系統公共資源
        Map<String,String> map=new HashMap<>();
        map.put("/user/login","anon");       //設定為公共資源,防止登入死循環
        map.put("/user/register","anon");
        map.put("/register.jsp","anon");
        map.put("/**","authc");  //authc 這個資源需要認證和授權
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        //預設界面路徑
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        return shiroFilterFactoryBean;
    }
    /**
     * 安全管理器
     * @return defaultWebSecurityManager
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    /**
     *
     * @return
     */
    @Bean("realm")
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        //修改憑證校驗比對器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //設定加密算法為md5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //設定散列次數
        credentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(credentialsMatcher);
        return customerRealm;
    }
}      

11.自定義realm

package com.hao.springboot.shiro.realm;

import com.hao.springboot.entity.User;
import com.hao.springboot.service.UserService;
import com.hao.springboot.utils.ApplicationContextUtils;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.util.ObjectUtils;

import java.util.Arrays;

/**
 * @author:抱着魚睡覺的喵喵
 * @date:2020/12/29
 * @description:    自定義realm完成使用者認證和授權
 */
public class CustomerRealm extends AuthorizingRealm {
    /**
     * 使用者授權
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("調用權限認證:"+principalCollection);
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        if ("Ronin".equals(primaryPrincipal)){
            simpleAuthorizationInfo.addRoles(Arrays.asList("user","admin","shaper"));
            simpleAuthorizationInfo.addStringPermission("user:add:*");
            return simpleAuthorizationInfo;
        }
//        else if("tom".equals(primaryPrincipal)){
//            simpleAuthorizationInfo.addRole("admin");
//            return simpleAuthorizationInfo;
//        }
        return null;
    }

    /**
     * 使用者認證
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String principal = (String) authenticationToken.getPrincipal();

        //在工廠中擷取業務對象
        UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
        User user = userService.findByUserName(principal);
        if (!ObjectUtils.isEmpty(user)){
            return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
        }
        return null;
    }
}      

12.編寫salt(因為要使用md5+salt+散列對密碼加密)

package com.hao.springboot.utils;

import java.util.Random;

/**
 * @author:抱着魚睡覺的喵喵
 * @date:2020/12/29
 * @description:
 */
public class SaltUtil {
    /**
     * 生成salt的靜态方法
     * @param n
     * @return
     */
    public static String getSalt(int n){
        char[] chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()".toCharArray();
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char aChar = chars[new Random().nextInt(chars.length)];
            stringBuilder.append(aChar);
        }
        return stringBuilder.toString();
    }
//測試一下
    public static void main(String[] args) {
        System.out.println(getSalt(8));
    }
}      

13.我們想要在自定義realm中的doGetAuthenticationInfo()方法調用service層,是以需要

使用靜态類擷取

package com.hao.springboot.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author:抱着魚睡覺的喵喵
 * @date:2020/12/29
 * @description:
 */
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context=applicationContext;
    }
    //
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }
}      

14,測試

通路aaaa

Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01

強制到登入頁面

Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01

以下是事先的資料

Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01
Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01

輸入Ronin && 123登入

Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01

可以看到Ronin使用者能夠看所有的權限資訊

接着點選退出

輸入tom && 123

Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01

點選登入

Shiro+springboot+mybatis(md5+salt+散列)認證與授權-01

可以發現tom使用者什麼都看不到,因為沒有賦給tom使用者任何權限

使用代碼控制角色權限

@Controller
@RequestMapping("/limit")
public class LimitController {

    @RequestMapping("/save")
    @RequiresRoles(value = {"admin","user"})
    @RequiresPermissions("user:update:*")
    public String save(){
//        Subject subject = SecurityUtils.getSubject();
//        if (subject.hasRole("admin")) {
//            System.out.println("儲存訂單!");
//        }else {
//            System.out.println("無權通路");
//        }
        System.out.println("進入方法");
        return "redirect:/index.jsp";
    }
}      
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context=applicationContext;
    }
    //
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }
}      

2021一起加油;