white-goo
文章7
标签6
分类1
shiro整合jwt

shiro整合jwt

首先肯定是导包

  • shiro和springboot整合
  • jwt依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>

然后就是shiro的配置了,jwt工具类就不多说了

ShiroConfig

Realm

  • Realm是授权和验证的核心逻辑类
  • 这里要准备一个PassWordRealm和JWTRealm,分别用来验证密码登录和jwt登录
@Slf4j
public class PassWordRealm extends AuthorizingRealm {

//这个方法一定要重写,后面Realm和Token匹配的时候会调用
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}

//这里没有做授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

return simpleAuthorizationInfo;
}

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

//这个地方用的Token是shiro自带的token
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
//拿到用户名去数据库查询,这里可以走数据库查,此处为了方便直接new,User的代码放在最后
User user = new User("UserName","PassWord");
if(user == null){
throw new AuthenticationException("用户名错误");
}

//这个info返回值有三个参数,
//第一个参数是user,可以不写,也可随意写
//第三个参数是Realm的名字,他会作为Key,第一个参数作为value,一起被放到Map里面
//第二个参数是校验参数,也就是密码,校验规则会放到下面

return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
}
}
@Slf4j
public class JwtRealm extends AuthorizingRealm {

@Autowired
private UserService userService;

@Override
public boolean supports(AuthenticationToken token) {

return token instanceof JwtToken;
}

//无授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

return simpleAuthorizationInfo;
}

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {


//这边还要做一个JWTToken,代码会放在下main
//这里是从自定义的JWTToken里获取token
String token = authenticationToken.getCredentials().toString();
//从token里获取用户名
String username = JwtUtil.getUsername(token);

//这里应该根据用户名从数据库查询出User,此处为了方便直接new
User user = new User();
if(user == null){
throw new AuthenticationException("用户不存在");
}

//对token的合法性进行校验
if (!JwtUtil.verify(token)) {
throw new AuthenticationException("认证异常");
}

//这里的校验规则和PassWordRealm的差不多,也放在下面讲
return new SimpleAuthenticationInfo(token, token,getName());
}
}

JwtToken

public class JwtToken implements AuthenticationToken {

private String jwtToken;

public JwtToken(String jwtToken) {
this.jwtToken = jwtToken;
}

//这两个方法的返回值于Realm中InFo的第二个参数要对应
@Override
public Object getPrincipal() {
return jwtToken;
}

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

这里说一下上面的校验规则:

//这是SimpleCredentialsMatcher类里面的方法
//传入的token为UserNamePassWordToken或者是自定义的JwtToken,这取决于你用那种方式登录
//传入的info就是Relam验证方法最后返回的SimpleAuthenticationInfo
//从代码可以看出,他是分别调用了Info和token的getCredentials,
//而UserNamePassWordToken的这个方法返回的是你创建UserNamePassWordToken时传入的密码,如果是自定义token就是自己重写这个方法,如上面的代码所示
//而info的这个方法,返回的是你在Realm中创建SimpleAuthenticationInfo时传入的第二个参数
//然后这拿着两个参数做Equals
//所以,对于UserNamePassWordToken,SimpleAuthenticationInfo的第二个参数一定是你的密码
//而对于自定义的JwtToken,SimpleAuthenticationInfo的第二个参数就可以随意些,只要和token里面getCredentials()方法的返回值一致就可以
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials);
}

JwtFilter

​ 这个类是自定义的拦截规则,在下面的ShiroConfig里面会具体讲

public class JwtFilter extends FormAuthenticationFilter {

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

//被拦截的请求会进入到这个方法,先从request中拿到cookie,因为我把登录成功后的token放到cookie里了,你可以根据情况进行调整
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
Cookie[] cookies = httpServletRequest.getCookies();
//拿到cookie后便利cookie拿到token
if (cookies != null) {
for (Cookie cookie : cookies) {
if(cookie.getName().contains("Token")){
try {
//如果有token就进行Jwt验证,这个login方法最终会进到我们自定义的ModularRealmAuthenticator类中,最终会进到我们的JwtRealm中的验证方法
getSubject(request,response).login(new JwtToken(cookie.getValue()));
//如果JwtRealm中的验证一切正常,就返回true,反之,就捕获我们在JwtRealm里面的验证方法里面抛出的异常
return true;
} catch (AuthenticationException e) {
return false;
}
}
}
}
/**
* 没有cookie 就返回false,会重定向到在ShiroConfig中配置的连接
*/
return false;
}
}

UserModularRealmAuthenticator

public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {


//这个类是确定传过来的token用哪个Realm进行验证的
//JwtFilter中的login方法就会进到这里
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {

//这里会验证realm是否是空的
assertRealmsConfigured();

//不为空就获取全部realm
Collection<Realm> realms = getRealms();
//用来放置登录相关的realm
Collection<Realm> typeRealm = new ArrayList<>();


try {
//这个地方直接对传进来的token进行强转,因为系统内只有两个token,如果报了类型转换异常,就肯定是另外一个,所以只要在catch内强转成另外一个就行了
JwtToken jwtToken = (JwtToken) authenticationToken;

//如果是jwtToke,那就遍历所有的Realm,找到我们自定义的JwtRealm
for (Realm realm : realms) {
if(realm.getName().contains("Jwt")){
typeRealm.add(realm);
}
}
//这个方法最终会进到JwtRealm的验证方法
return doSingleRealmAuthentication(typeRealm.iterator().next(), jwtToken);

} catch (ClassCastException e) {

UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
//跟上面一样,取出相应的Realm
for (Realm realm : realms) {
if(realm.getName().contains("PassWord")){
typeRealm.add(realm);
}
}

//验证密码的方式可能有多中,所以提供了单Realm验证,和多Realm验证两种方式,本案例只有一个PassWordRealm
if(typeRealm.size() == 1){
return doSingleRealmAuthentication(typeRealm.iterator().next(), usernamePasswordToken);
}else {
return doMultiRealmAuthentication(typeRealm, usernamePasswordToken);
}

}

}
}

ShiroConfig

@Configuration
public class ShiroConfig {


//首先,把自定义的Realm和ModularRealmAuthenticator配进来
@Bean
public Realm JwtRealm(){
return new JwtRealm();
}
@Bean
public Realm PassWordRealm(){
return new PassWordRealm();
}
@Bean
public UserModularRealmAuthenticator UserModularRealmAuthenticator(){

UserModularRealmAuthenticator userModularRealmAuthenticator = new UserModularRealmAuthenticator();
userModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return userModularRealmAuthenticator;
}


//配置ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

//shiro 内置的过滤器
/*
anon:无需认证就能访问
authc:必须认证才能访问
user: 必须拥有 记住我 才能访问
perms: 拥有对某个资源的权限才能访问
role: 拥有某个角色的权限才能访问
*/
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

filterChainDefinitionMap.put("/register","anon");
filterChainDefinitionMap.put("/login", "anon");
//静态资源一定要记得配置,不然会被shiro拦截
filterChainDefinitionMap.put("/js/**/*","anon");
filterChainDefinitionMap.put("/css/**/*","anon");
filterChainDefinitionMap.put("/icon/**/*","anon");
filterChainDefinitionMap.put("/img/**/*","anon");
filterChainDefinitionMap.put("/fonts/**/*","anon");

//其余的所有请求都走我们的JwtFilter
filterChainDefinitionMap.put("/**", "jwt");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

/**
* 设置认证失败的路径
* JwtFilter里返回false后重定向的路径就是这个,
* 这是一个登录页的路径
*/
shiroFilterFactoryBean.setLoginUrl("/index");
/**
* 设置未经授权的路径
*/
shiroFilterFactoryBean.setUnauthorizedUrl("/index");

/**
* 设置自定义过滤器
* 这里的key就是上面的过滤其名称,value就是自定义的JwtFilter
*/
HashMap<String, Filter> objectObjectHashMap = new HashMap<>(1);
objectObjectHashMap.put("jwt", new JwtFilter());

shiroFilterFactoryBean.setFilters(objectObjectHashMap);

return shiroFilterFactoryBean;
}

//配置DefaultWebSecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(
@Qualifier("JwtRealm") Realm jwtRealm,
@Qualifier("PassWordRealm") Realm passWordRealm,
@Qualifier("UserModularRealmAuthenticator") UserModularRealmAuthenticator userModularRealmAuthenticator){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

defaultWebSecurityManager.setAuthenticator(userModularRealmAuthenticator);

//设置realm
List<Realm> list = new ArrayList<>();
list.add(jwtRealm);
list.add(passWordRealm);
defaultWebSecurityManager.setRealms(list);

/**
* 关闭shiro自带的session
* shiro的session机制后续会补充
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);

return defaultWebSecurityManager;
}

到此,整合就已经全部结束了,最后写一点demo进行一下验证就行了

Controller

@PostMapping("/login")
@ResponseBody
public String login(User user, HttpServletResponse response){
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));
//登录成功就设置一个cookie
response.addCookie(new Cookie("Token",JwtUtil.sign(user.getUsername())));
return "登录成功";
}catch (IncorrectCredentialsException e){
return "密码错误";
}catch (AuthenticationException e) {
return e.getMessage();
}
}

@RequestMapping({"/index","/"})
public String index(){
//判断是否已登录
if (SecurityUtils.getSubject().isAuthenticated()) {
return "success";
}
return "index";
}

@RequestMapping("/logOut")
public String logOut(HttpServletResponse httpServletResponse){
Cookie cookie = new Cookie("Token","");
cookie.setMaxAge(0);
httpServletResponse.addCookie(cookie);
SecurityUtils.getSubject().logout();
return "index";
}

Bean.User

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class User {

private Integer id;
@NotNull
@NotEmpty
private String username;
@NotNull
@NotEmpty
private String password;

}

index页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>

demo到这里就结束了,如有疑问,可以在评论区探讨,有不同意见请轻喷( •̀ ω •́ )✧

本文作者:white-goo
本文链接:https://white-goo.github.io/2021/08/01/shiro%E6%95%B4%E5%90%88jwt/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可