Spring + Shiro + JWT 开发简单的认证与授权(单角色)

前言


单角色的意思就是,一个用户只有一个角色,用户与角色处于一对一关系,因此,角色可以放到用户表的字段中冗余。在一些小型项目中,需求也许没有那么复杂,因此只考虑单角色。

以下项目的 gitee 地址,便于需要时直接 clone:

https://gitee.com/jiang_chun_bo/shiro-single-role.git

创建数据表


建立一个数据库,复制到 mysql 命令行执行就行了。本文是使用 MySQL 的 test 数据库。

CREATE TABLE `sys_user`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NULL,
  `password` varchar(255) NULL,
  `role` varchar(255) NULL,
  PRIMARY KEY (`id`)
);

Maven 工程 pom.xml 文件


配置可参考

https://blog.csdn.net/qq_39291919/article/details/108269682

SpringBoot 配置文件


配置一下数据源即可。IP、端口、数据库名、用户名、密码根据需要改。

spring:
   datasource:
      url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
      username: root
      password: root

POJO 与常量


UserDO.java

sys_user 的 Java 实体类

@TableName(value = "sys_user")
@Getter
@Setter
public class UserDO {
    
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    
    @TableField(value = "username")
    private String username;
    
    @TableField(value = "password")
    private String password;
    
    @TableField(value = "role")
    private String role;
   
}

ResponseCode.java

ResponseVO.java

ResponseVO 为返回给前端的包裹对象;ResponseCode 进行了返回码的定义。具体可见:

https://blog.csdn.net/qq_39291919/article/details/106630987

LoginDTO.java

前端传递给后端的对象

@Getter
@Setter
public class LoginDTO {
    private String username;
    private String password;
}

LoginVO.java

后端传递给前端的对象

@Getter
@Setter
public class LoginVO {
    private String token;
}

Mapper


UserMapper.java

只需要继承 MybatisPlus 的 BaseMapper 即可

@Repository
public interface UserMapper extends BaseMapper<UserDO> {

}

SpringBeanUtil

为了能够在没有被 Spring 容器管理的对象中获取 Bean,自定义容器工具,可参考如下文章代码:

https://blog.csdn.net/qq_39291919/article/details/108415183

JsonWebToken

Shiro 框架提供的 UsernamePasswordToken 已经不适用了,JWT 是不存储敏感等敏感信息的。自己实现一个。Credentials 目前不返回任何有效值,直接返回 null。

public class JsonWebToken implements HostAuthenticationToken {
	
    private String username;

    private String host;

	public JsonWebToken(String username, String host) {
		super();
		this.username = username;
		this.host = host;
	}

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

	@Override
	public Object getCredentials() {
		return null;
	}

	@Override
	public String getHost() {
		return host;
	}

}

JwtFilter

JwtFilter 是当用户请求非匿名权限的 URL 时会执行的过滤器。

@Component
public class JwtFilter extends BasicHttpAuthenticationFilter {
	
	/**
	 * 每个请求都会先判断 isAccessAllowed(),该方法返回 false,之后会走 onAccessDenied()
	 */
	@Override
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
		return false;
	}
	
	/**
	 * super 方法会先 createToken,使用该 token 进行登录
	 */
	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
		super.executeLogin(request, response);
		return true;
	}

	/**
	 * 重写 createToken,根据 JWT 头部获取
	 */
	@Override
	protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
		HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//		获取 IP 地址
		String host = request.getRemoteHost();
//		获取 JWT 头部值,如果为空,则 username 以空值返回
		String jwtHeader =  httpServletRequest.getHeader("X-Token");
		if(jwtHeader == null || jwtHeader.length() == 0) {
			return new JsonWebToken("", host);
		}
//		如果 header 不为空,那么尝试解析 username
		String username = JWT.decode(jwtHeader).getClaim("username").asString();
		if (username == null || username.length() == 0) {
            return new JsonWebToken("", host);
        }
		
		return new JsonWebToken(username, host);
	}

    @Override
    protected boolean preHandle(ServletRequest req, ServletResponse res) throws Exception {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
            response.setStatus(HttpStatus.OK.value());
            response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
                request.getHeader(HttpHeaders.ORIGIN));
            response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
                request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
            response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
                request.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS));
            response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
                request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
        }
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
            request.getHeader(HttpHeaders.ORIGIN));
        return super.preHandle(request, response);
    }
}

Realm


Realm 是 Shiro 中的概念,当执行 JWTFilter.executeLogin() 方法中的 getSubject().login() 时,会进入Realm 进行认证信息与权限信息获取。

@Component
public class DefaultAuthorizingRealm extends AuthorizingRealm {

	@Autowired
	UserMapper userMapper;

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		/* 取得本用户的 username */
		UserDO user = (UserDO)SecurityUtils.getSubject().getPrincipal();

		/* 添加 role,这里只有 1 个 role */
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		info.addRole(user.getRole());
		return info;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

//    	这里的 token 其实是 JsonWebToken 的实例
		String username = token.getPrincipal().toString();
//      根据 username 查询系统用户
		UserDO user = userMapper.selectOne(new QueryWrapper<UserDO>().lambda().eq(UserDO::getUsername, username));
		if (user == null) {
			throw new UnknownAccountException("用户不存在");
		}
		return new SimpleAuthenticationInfo(user, null, getName());
	}

}

ShiroConfiguration


注意:此处代码在导入包时可能会报错,因为 Shiro 中使用的是 org.apache.shiro.mgt.SecurityManager,而编译器默认会使用 java.lang.SecurityManager,需要手动导入。

import org.apache.shiro.mgt.SecurityManager;
@Configuration
public class ShiroConfiguration {

    @Autowired
    DefaultAuthorizingRealm realm;

    /**
     * 生成一个随机值用户 JWT 的加密
     * 
     * @return 用于 JWT 加密的随机值
     */
    @Bean
    public String secret() {
        return UUID.randomUUID().toString();
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        /* 定义过滤器链 */
        LinkedHashMap<String, String> filterChain = new LinkedHashMap<>();
        /* 定义匿名用户即可访问的 URL,anon 意思是 anonymous */
        filterChain.put("/user/login", "anon");
        filterChain.put("/doc.html", "anon");
        filterChain.put("/**/**.js", "anon");
        filterChain.put("/**/**.css", "anon");
        filterChain.put("/swagger-resources/**", "anon");
        filterChain.put("/webjars/**", "anon");
        filterChain.put("/v2/**", "anon");
        /* jwt 必须放在最后 */
        filterChain.put("/**", "jwt");

        /* 添加 JWT 过滤器,并命名为 jwt */
        Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
        filterMap.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChain);
        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);

        /* 关闭shiro自带的session */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * 
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 开启aop注解支持
     * 
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

SwaggerConfiguration


如果不需要前后端在线文档,可以跳过这一步。

@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfiguration {

    public List<Parameter> globalOperationParameters() {
        List<Parameter> params = new ArrayList<Parameter>();
        Parameter parameter = new ParameterBuilder()
            .name("X-Token")
            .description("JSON Web Token")
            .modelRef(new ModelRef("string"))
            .parameterType("header")
            .required(false)
            .build();
        params.add(parameter);
        return params;
    }

    @Bean
    public Docket Api() {
        ApiInfo apiInfo = new ApiInfoBuilder()
            .build();
        return new Docket(DocumentationType.SWAGGER_2)
            .globalOperationParameters(globalOperationParameters())
            .apiInfo(apiInfo)
            .select()
            .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
            .paths(PathSelectors.any())
            .build();
    }
}

UserService


UserService 接口

public interface UserService {
    LoginVO login(LoginDTO loginDTO);
}

UserServiceImpl 实现类

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;
    
    @Autowired
    String secret;

    @Override
    public LoginVO login(LoginDTO loginDTO) {
        UserDO user = userMapper
            .selectOne(new QueryWrapper<UserDO>().lambda().eq(UserDO::getUsername, loginDTO.getUsername()));
        /* (1) 判断用户名是否存在 */
        boolean exist = user != null;
        if (!exist) {
            throw new UnknownAccountException("用户名不存在");
        }

        /* (2) 判断密码是否错误 */
        boolean valid = user.getPassword().equals(loginDTO.getPassword());
        if (!valid) {
            throw new IncorrectCredentialsException("密码错误");
        }

        /* (3) 获取该 user 的角色 */
        String role = user.getRole();
        String token = JWT.create()
            .withClaim("username", user.getUsername())
            .withClaim("role", role)
            .withExpiresAt(DateUtil.offsetSecond(new Date(), 60))
            .sign(Algorithm.HMAC256(secret));
        LoginVO loginVO = new LoginVO();
        loginVO.setToken(token);
        return loginVO;
    }

}

创建 UserController


@RestController
@Api(tags = "用户管理")
public class UserController {

    @Autowired
    UserService userService;
    
    @PostMapping("/user/login")
    @ApiOperation("登录")
    public ResponseVO login(@RequestBody LoginDTO loginDTO) {
        return ResponseVO.success(userService.login(loginDTO));
    }
    
    @PostMapping("/echo")
    @ApiOperation("测试")
    @RequiresRoles(value = {"admin"})
    public ResponseVO echo() {
        return ResponseVO.success("SUCCESS");
    }
}

启动类


需要加上 Mybatis 的注解 MapperScan 进行 Mapper 的扫描

@SpringBootApplication
@MapperScan(basePackages = {"com.jiangchunbo.dao"})
public class StartEntry {

    public static void main(String[] args) {
        SpringApplication.run(StartEntry.class, args);
    }
}

测试


用户名不存在

密码错误

登录成功

登录成功之后返回 token

无权限访问

过期访问

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罐装面包

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值