Spring Boot快速整合Shiro

文章目录

  • Spring Boot快速整合Shiro
    • 1、创建Demo
    • 2、Shiro实现登陆拦截
    • 3、Shiro实现用户认证
    • 4、Shiro整合Mybatis-Plus
    • 5、Shiro整合MD5盐值加密
    • 6、Shiro实现授权
    • 7、Shiro整合JWT+MD5

Spring Boot快速整合Shiro

1、创建Demo

  • 创建Spring Boot项目

我的用的是2.7.9版本的

  • 导入坐标
<dependencies>
	<dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-thymeleafartifactId>
    dependency>
    
    <dependency>
        <groupId>org.apache.shirogroupId>
        <artifactId>shiro-springartifactId>
        <version>1.11.0version>
    dependency>
dependencies>
  • 简单配置

自定义Realm

// 自定义
public class UserRealm extends AuthorizingRealm {

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权=》doGetAuthorizationInfo");
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了认证=》doGetAuthenticationInfo");
        return null;
    }
}

ShiroConfig配置类

@Configuration
public class ShiroConfig {

    // 创建realm类,需要自定义类
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    // DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 关联UserRealm
        securityManager.setRealm(userRealm);

        return securityManager;
    }

    // ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);

        return bean;
    }
}
  • 设置几个页面

add.html

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<h1>addh1>
body>
html>

del.html

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<h1>delh1>
body>
html>

index.html

DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<h1>首页h1>
<p th:text="${msg}">p>
<a th:href="@{/user/add}">adda> <br>

<a th:href="@{/user/del}">dela>

body>
html>
  • 配置Controller
@Controller
public class MyController {

    @RequestMapping("/")
    public String toIndex(Model model){
        model.addAttribute("msg", "Hello World");
        return "index";
    }

    @RequestMapping("/user/add")
    public String add(){
        return "user/add";
    }

    @RequestMapping("/user/del")
    public String del(){
        return "user/del";
    }
}

这样就在启动项目后,就可以看到,现在是可以自由切换,下面进行更深的配置

Spring Boot快速整合Shiro_第1张图片

2、Shiro实现登陆拦截

  • 新增登录页面
DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<h1>登录页面h1>
<form action="">
  <p>用户名:<input type="text" name="username"> p><br>
  <p>密码:<input type="password" name="password"> p><br>
  <p><input type="submit"> p>
form>
body>
html>
@RequestMapping("/toLogin")
public String toLogin(){
    return "login";
}
  • 配置ShiroConfig
@Configuration
public class ShiroConfig {

    // 创建realm类,需要自定义类
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    // DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 关联UserRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    // ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);

        // 添加Shiro的内置过滤器
        /**
         *  anno:无需认证就可以访问
         *  authc:必须认证了才能访问
         *  user:必须拥有记住我功能才能用
         *  perms:拥有对某个资源的权限才能访问
         *  role:拥有某个角色权限才能访问
         */

        // 使用map来指定过滤器
        Map<String, String> map = new LinkedHashMap<String, String>();
        // map.put("/user/*", "authc")
        map.put("/user/add", "authc");
        map.put("/user/del", "authc");

        bean.setFilterChainDefinitionMap(map);

        // 设置登录页面
        bean.setLoginUrl("/toLogin");
        return bean;
    }
}

如上设置了过滤器还有,没有认证的要跳转到登录的页面

3、Shiro实现用户认证

  • 在Controller中加一个login
@RequestMapping("/login")
public String login(String username, String password, Model model){
    // 获取当前用户
    Subject subject = SecurityUtils.getSubject();

    // 封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);

    // 登录 执行登录的方法
    try {
        subject.login(token);
        return "index";
    } catch (UnknownAccountException e) { // 用户名不存在
        model.addAttribute("msg", "用户名错误");
        return "login";
    } catch (IncorrectCredentialsException e){// 密码错误
        model.addAttribute("msg", "密码错误");
        return "login";
    }
}
  • 自定义UserRealm中做认证处理
// 自定义
public class UserRealm extends AuthorizingRealm {

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权=》doGetAuthorizationInfo");
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了认证=》doGetAuthenticationInfo");
        // 用户名、密码  对接数据库

        String username = "root";  // 先伪造一手
        String password = "123";
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;

        // 如果账号不正确就认证失败
        if(!userToken.getUsername().equals(username)) return null;

        // 密码认证是Shiro帮你做了
        return new SimpleAuthenticationInfo("", password, "");
    }
}
DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<p th:text="${msg}" style="color: red;">p>
<h1>登录页面h1>
<form th:action="@{/login}">
  <p>用户名:<input type="text" name="username"> p><br>
  <p>密码:<input type="password" name="password"> p><br>
  <p><input type="submit"> p>
form>
body>
html>

4、Shiro整合Mybatis-Plus

  • 坐标

 <dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>8.0.32version>
dependency>


<dependency>
    <groupId>log4jgroupId>
    <artifactId>log4jartifactId>
    <version>1.2.17version>
dependency>

<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druid-spring-boot-starterartifactId>
    <version>1.1.23version>
dependency>

<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.4.2version>
dependency>
  • 配置文件application.yaml
spring:
  datasource:
    username: root
    password: lige0612
    url: jdbc:mysql://localhost:3306/codingmore?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      #初始化连接池大小
      initial-size: 5
      #配置最小连接数
      min-idle: 5
      #配置最大连接数
      max-active: 200
      #配置连接等待超时时间
      max-wait: 60000
      #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      #配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      #测试连接
      validation-query: SELECT 1 FROM DUAL
      #申请连接的时候检测,建议配置为true,不影响性能,并且保证安全
      test-while-idle: true
      #获取连接时执行检测,建议关闭,影响性能
      test-on-borrow: false
      #归还连接时执行检测,建议关闭,影响性能
      test-on-return: false
      #是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭
      pool-prepared-statements: false
      #开启poolPreparedStatements后生效
      max-pool-prepared-statement-per-connection-size: 20
      #配置扩展插件,常用的插件有=>stat:监控统计  log4j:日志  wall:防御sql注入
      filters: stat,wall,slf4j
      #打开mergeSql功能;慢SQL记录
      connection-properties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      #配置DruidStatFilter
      web-stat-filter:
        enabled: true
        url-pattern: "/*"
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
      #配置DruidStatViewServlet
      stat-view-servlet:
        url-pattern: "/druid/*"
        allow: 127.0.0.1
        #登录名
        login-username: root
        #登录密码
        login-password: root
        enabled: true
server:
  port: 8080
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID
  type-aliases-package: com.lzy.shiro_springboot.pojo
  • 数据库
    Spring Boot快速整合Shiro_第2张图片
  • 老生常谈

pojo

public class User {
    private Integer id;
    private String name;
    private String pwd;

    public User(Integer id, String name, String password) {
        this.id = id;
        this.name = name;
        this.pwd = password;
    }

    public User() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return pwd;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + pwd + '\'' +
                '}';
    }
}

mapper

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

service 和 serviceImpl

public interface UserService extends IService<User> {
}

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
}
  • 自定义realm接入数据库
// 自定义
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权=》doGetAuthorizationInfo");
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了认证=》doGetAuthenticationInfo");
        // 用户名、密码  对接数据库

        UsernamePasswordToken userToken = (UsernamePasswordToken) token;

        // 连接真实的数据库
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        lqw.eq(User::getName, userToken.getUsername());
        User user = userService.getOne(lqw);

        if(user == null) {
            return null;
        }
        // 密码认证是Shiro帮你做了,Shiro加密
        return new SimpleAuthenticationInfo("", user.getPassword(), "");
    }
}

此时就可以将数据库中的数据接入到Realm中,实现输入的账号密码对接数据库

5、Shiro整合MD5盐值加密

  • 数据库中加入盐值的字段

在这里插入图片描述

  • pojo对应的实体类
    Spring Boot快速整合Shiro_第3张图片

  • Md5Utils

public class Md5Utils {
    public static String getSalt(int n){
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char c = chars[new Random().nextInt(chars.length)];
            stringBuilder.append(c);
        }
        return stringBuilder.toString();
    }

    public static List<String> encryption(String password){
        List<String> msg = new ArrayList<>();
        String salt = getSalt(10);
        msg.add(salt);

        Md5Hash MD5 = new Md5Hash(password, salt, 1024);
        msg.add(MD5.toHex());
		// 其中msg[0]是盐值,msg[1]是加密后的密码,可以一并保存至数据库中。
        return msg;
    }
}
  • 配置密码匹配器

ShiroConfig

/**
 * 密码匹配器
 * @return HashedCredentialsMatcher
 */
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher(){
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    // 设置哈希算法名称
    matcher.setHashAlgorithmName("MD5");
    // 设置哈希迭代次数
    matcher.setHashIterations(1024);
    // 设置存储凭证(true:十六进制编码,false:base64)
    matcher.setStoredCredentialsHexEncoded(true);
    return matcher;
}

// 创建realm类,需要自定义类
@Bean
public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){
    UserRealm userRealm = new UserRealm();
    userRealm.setCredentialsMatcher(matcher);
    return userRealm;
}

Spring Boot快速整合Shiro_第4张图片

Spring Boot快速整合Shiro_第5张图片

  • 接入Md5加密

MyRealm

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行了认证=》doGetAuthenticationInfo");
    // 用户名、密码  对接数据库
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;
    // 连接真实的数据库
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
    lqw.eq(User::getName, userToken.getUsername());
    User user = userService.getOne(lqw);
    if(user != null) {
        return new SimpleAuthenticationInfo(user, user.getPwd(), ByteSource.Util.bytes(user.getSalt()), getName());
    }
    return null;
}

此时你可以使用Md5工具去生成几个例子,将数据库中的密码和盐值换成你生成,然后去试一试,如果要注册的时候,在注册逻辑中把密码加密,和盐值的逻辑加入即可

6、Shiro实现授权

  • 数据库

加一个权限的字段

Spring Boot快速整合Shiro_第6张图片

修改pojo对应的实体类

Spring Boot快速整合Shiro_第7张图片

进行授权判断
ShiroConfig

// ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    // 设置安全管理器
    bean.setSecurityManager(defaultWebSecurityManager);

    // 添加Shiro的内置过滤器
    /**
     *  anno:无需认证就可以访问
     *  authc:必须认证了才能访问
     *  user:必须拥有记住我功能才能用
     *  perms:拥有对某个资源的权限才能访问
     *  role:拥有某个角色权限才能访问
     */

    // 使用map来指定过滤器

    Map<String, String> map = new LinkedHashMap<>();
    // 授权
    map.put("/user/add", "perms[user:add]");
    map.put("/user/del", "perms[user:del]");

    // 拦截
    map.put("/user/*", "authc");

    bean.setFilterChainDefinitionMap(map);

    // 设置登录页面
    bean.setLoginUrl("/toLogin");
    // 未授权跳转
    bean.setUnauthorizedUrl("/noauth");
    return bean;
}

MyRealm

// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了授权=》doGetAuthorizationInfo");
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    // 拿到当前用户对象
    Subject subject = SecurityUtils.getSubject();
    // 拿到用户对象
    User currentUser = (User)subject.getPrincipal();
    // 对接数据库权限
    info.addStringPermission(currentUser.getPerms());
    return info;
}

现在就是先登录,然后根据你数据库中的权限去做授权,然后你就能访问你对应的页面了

7、Shiro整合JWT+MD5

找了好多的资料。最后发现一个差不多的,就这样吧

  • 坐标
<dependency>
  <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.16</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
</dependency>
  • 数据源
    application.yaml
>
   >org.apache.shiro>
   >shiro-spring>
   >1.3.2>
>
>
   >com.auth0>
   >java-jwt>
   >3.2.0>
>
>
   >org.projectlombok>
   >lombok>
>

>
   >cn.hutool>
   >hutool-all>
   >5.8.2>
>

>
   >mysql>
   >mysql-connector-java>
   >8.0.32>
>
>
   >com.alibaba>
   >druid-spring-boot-starter>
   >1.2.16>
>
>
   >com.baomidou>
   >mybatis-plus-boot-starter>
   >3.5.2>
>

三层

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserBean {
    private String username;
    private String password;
    private String role;
    private String permission;
}

@Mapper
public interface UserMapper extends BaseMapper<UserBean> {
}

public interface UserService extends IService<UserBean> {
}

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserBean> implements UserService {
}

JWTUtil

public class JWTUtil {

    // 过期时间5分钟
    private static final long EXPIRE_TIME = 5*60*1000;

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,5min后过期
     * @param username 用户名
     * @param secret 用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

restful

public class ResponseBean {
    
    // http 状态码
    private int code;

    // 返回信息
    private String msg;

    // 返回的数据
    private Object data;

    public ResponseBean(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

自定义异常类

public class UnauthorizedException extends RuntimeException {
    public UnauthorizedException(String msg) {
        super(msg);
    }

    public UnauthorizedException() {
        super();
    }
}

Controller

@RestController
public class WebController {

    private static final Logger LOGGER = LogManager.getLogger(WebController.class);

    private UserService userService;

    @Autowired
    public void setService(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResponseBean login(@RequestParam("username") String username,
                              @RequestParam("password") String password) {
        LambdaQueryWrapper<UserBean> lqw = new LambdaQueryWrapper<>();
        lqw.eq(UserBean::getUsername, username);
        UserBean userBean = userService.getOne(lqw);
        if (userBean.getPassword().equals(DigestUtil.md5Hex(password))) {
            return new ResponseBean(200, "Login success", JWTUtil.sign(username, DigestUtil.md5Hex(password)));
        } else {
            throw new UnauthorizedException();
        }
    }

    @GetMapping("/article")
    public ResponseBean article() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            return new ResponseBean(200, "You are already logged in", null);
        } else {
            return new ResponseBean(200, "You are guest", null);
        }
    }

    @GetMapping("/require_auth")
    @RequiresAuthentication
    public ResponseBean requireAuth() {
        return new ResponseBean(200, "You are authenticated", null);
    }

    @GetMapping("/require_role")
    @RequiresRoles("admin")
    public ResponseBean requireRole() {
        return new ResponseBean(200, "You are visiting require_role", null);
    }

    @GetMapping("/require_permission")
    @RequiresPermissions(logical = Logical.AND, value = {"view", "edit"})
    public ResponseBean requirePermission() {
        return new ResponseBean(200, "You are visiting permission require edit,view", null);
    }

    @RequestMapping(path = "/401")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ResponseBean unauthorized() {
        return new ResponseBean(401, "Unauthorized", null);
    }
}

这里在login的时候,判断输入进来的password的md5加密后的字符串是否与从数据库中查询的一样,一样的话,采取生成Token

  • 处理框架异常
@RestControllerAdvice
public class ExceptionController {

    // 捕捉shiro的异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public ResponseBean handle401(ShiroException e) {
        return new ResponseBean(401, e.getMessage(), null);
    }

    // 捕捉UnauthorizedException
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseBean handle401() {
        return new ResponseBean(401, "Unauthorized", null);
    }

    // 捕捉其他所有异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
        return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }
}
  • JWTToken
public class JWTToken implements AuthenticationToken {
    // 密钥
    private String token;

    public JWTToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}
  • 实现Realm
@Service
public class MyRealm extends AuthorizingRealm {

    private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    /**
     * 大坑!,必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        LambdaQueryWrapper<UserBean> lqw = new LambdaQueryWrapper<>();
        lqw.eq(UserBean::getUsername, username);
        UserBean user = userService.getOne(lqw);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole(user.getRole());
        Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
        simpleAuthorizationInfo.addStringPermissions(permission);
        return simpleAuthorizationInfo;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = JWTUtil.getUsername(token);

        if (username == null) {
            throw new AuthenticationException("token invalid");
        }
        LambdaQueryWrapper<UserBean> lqw = new LambdaQueryWrapper<>();
        lqw.eq(UserBean::getUsername, username);
        UserBean userBean = userService.getOne(lqw);
        if (userBean == null) {
            throw new AuthenticationException("User didn't existed!");
        }

        if (!JWTUtil.verify(token, username, userBean.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }

        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}

鉴权和认证最终都是在这里,这里我觉得认证和Spring Secuity是差不多的,只不过Spring Secuity可以一个api就搞完了,顺便还能验证加密的账户信息,而我这里在login是手动的去做的

  • JWTFilter
public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        return authorization != null;
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");

        JWTToken token = new JWTToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(request, response);
            }
        }
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 将非法请求跳转到 /401
     */
    private void response401(ServletRequest req, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/401");
        } catch (IOException e) {
            LOGGER.error(e.getMessage());
        }
    }
}

我觉得这个过滤器更像是一个路由,getSubject(request, response).login(token);这个还是重点

  • ShiroConfig
@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(realm);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");

        /*
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt");
        // 访问401和404页面不通过我们的Filter
        filterRuleMap.put("/401", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

首先是关掉了Shiro的默认的session,然后去做拦截器规则和拦截的配置,将我们前面配置的JwtFilter拿过来,意思就是把所有的请求都拦截到,统一放到JwtFilter去做处理

参考https://segmentfault.com/a/1190000039843857

你可能感兴趣的:(分享,spring,boot,java,spring)