赞
踩
花了几天了解Shiro框架(也不算太深入),根据网上资料做了一个Demo:SpringBoot 2.2.9.RELEASE+ Shiro + Jwt 实现登录认证。
1、这个Demo关注 登录授权(比较通用),基本不涉及权限控制,因为权限控制设计到具体业务。所以可以本Demo可以根据自身实际情况稍加修改,就可以作为前后端分离项目的登录模块。
2、本博客适用于对Shiro和Jwt有了解,起码对于几个关键的类的作用及方法有了解,下面我不会展开,只会对Demo的思路和代码做分析。
1、Shiro原本的登录认证的流程
subject.login(new UsernamePasswordToken(username, password)); 这是执行登录认证的关键代码,然后Shiro内部会在对应的Realm里面的doGetAuthenticationInfo()中根据username从数据库中查找正确的User信息(包括密码信息,密码可以被加密存储)。然后将正确的User信息封装成AuthenticationInfo对象。在比较器(CredentialsMatcher)的doCredentialsMatch()方法中按照一定规则进行密码匹配(比较)。如果匹配成功,就登录成功。
登录成功后,subject的登录信息会被存储session中。那么下次访问受限的资源或接口,便不需要再登陆了。
2、加入Jwt后的登录认证的流程
Jwt和Shiro原本的登录认证有冲突吗? 其实Jwt本质上就是一种特殊的token而已。换言之,Shiro + Jwt的意思就是使用token(Jwt)替换到Shiro原本的session,使用token的服务端更加适用于前后端分离的项目。不管前端是vue项目还是Android APP等等。
替换之后的流程是什么样的? 首先,第一次登录,我们需要用户输入username和password(根据实际情况,此处简单举例),然后按照Shiro原本的登录认证流程登录。如果登录成功,服务端返回一个Jwt字符串(相当于签发一个令牌)给前端。下次用户访问服务端受限的资源,只要携带正确合法的Jwt就可以访问了。
实现的主要思路是怎样的?我们需要实现一个过滤器,拦截所有请求,对于请求头中含有Jwt的请求进行特殊处理,我们不使用Shiro默认登录流程来处理,而是使用我们自定义的处理流程,包括实现对应的Token,Realm,CredentialsMatcher。当认证通过之后,将请求放行到Controller层。
经过一顿分析,本Demo主要划分3个部分:注册、密码登录、携带令牌(jwt)访问。其中,“携带令牌(jwt)访问”这个部分就是Shiro整合Jwt的核心逻辑。
数据库相关等其他依赖我就不贴出来了,详情参照Demo的Github源码:https://github.com/passerbyYSQ/SpringBoot_Shiro_Jwt
【推荐】Shiro的项目实战使用参考(这个项目打磨了比较久,Shiro的使用可参考该项目):
- <!-- apache为SpringBoot整合shiro提供的starter -->
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-spring-boot-starter</artifactId>
- <version>1.5.3</version>
- </dependency>
-
- <!-- Shiro默认的缓存管理 -->
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-ehcache</artifactId>
- <version>1.5.3</version>
- </dependency>
-
- <!-- JWT -->
- <dependency>
- <groupId>com.auth0</groupId>
- <artifactId>java-jwt</artifactId>
- <version>3.3.0</version>
- </dependency>
注册的时候,我们需要将用户输入的明文密码通过Md5Hash加密处理,传入加密的随机盐(每个用户不一样,需要存到用户表)和哈希散列的次数(每个用户都一样)。
那么在使用密码登录时,我们需要以相同的加密规则加密用户输入的密码,然后与数据库中存储的注册时的加密密码,进行比对。如果一样,登录成功。
UserController
- /**
- * 通过注解实现权限控制
- * 在方法前添加注解 @RequiredRoles 或 @RequiredPermissions
- *
- * @author passerbyYSQ
- * @create 2020-08-20 23:54
- */
- @Controller
- @RequestMapping("/user")
- public class UserController {
-
- @Autowired
- private UserService userService;
-
- /**
- * 用户注册
- * @param user
- * @return
- */
- @PostMapping("/register")
- public ResponseEntity<String> register(User user) {
- // 参数判断省略
- // ...
-
- try {
- userService.register(user);
- return ResponseEntity.ok().build();
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- // 错误提示信息省略...
- return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body("客户端传参错误");
- }
-
- }
UserServiceImpl
- /**
- * @author passerbyYSQ
- * @create 2020-08-21 11:02
- */
- @Service("userService")
- @Transactional // 开启事务。有需要再开启
- public class UserServiceImpl implements UserService {
-
- @Autowired
- private UserDAO userDAO;
-
- @Override
- public void register(User user) {
- // 8个字符的随机字符串,作为加密的随机盐
- String salt = RandomUtil.generateStr(8);
- // 需要保存到数据库,第一次登录(认证)比较时需要使用
- user.setSalt(salt);
-
- // Md5Hash默认将随机盐拼接到源字符串的前面,然后使用md5加密,再经过x次的哈希散列
- // 第三个参数(hashIterations):哈希散列的次数
- Md5Hash md5Hash = new Md5Hash(user.getPassword(), user.getSalt(), 1024);
- user.setPassword(md5Hash.toHex());
-
- // 保存
- userDAO.save(user);
- }
- }
UserController
- /**
- * 通过注解实现权限控制
- * 在方法前添加注解 @RequiredRoles 或 @RequiredPermissions
- *
- *
- * @author passerbyYSQ
- * @create 2020-08-20 23:54
- */
- @Controller
- @RequestMapping("/user")
- public class UserController {
-
- @Autowired
- private UserService userService;
-
-
- /**
- * 用户登录(身份认证)
- * Shiro会缓存认证信息
- *
- * @param username
- * @param password
- * @return
- */
- @PostMapping("/login")
- public ResponseEntity<String> login(String username, String password) {
- // 前期的注入工作已经由SpringBoot完成了
- // 获取当前来访用户的主体对象
- Subject subject = SecurityUtils.getSubject();
-
- try {
- // 执行登录,如果登录失败会直接抛出异常,并进入对应的catch
- subject.login(new UsernamePasswordToken(username, password));
-
- // 获取主体的身份信息
- // 实际上是User。为什么?
- // 取决于LoginRealm中的doGetAuthenticationInfo()方法中SimpleAuthenticationInfo构造函数的第一个参数
- User user = (User) subject.getPrincipal();
-
- // 生成jwt
- String jwt = userService.generateJwt(user.getUsername());
-
- // 将jwt放入到响应头中
- return ResponseEntity.ok().header("token", jwt).build();
-
- } catch (UnknownAccountException e) {
- // username 错误
- e.printStackTrace();
- return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("username不存在");
- } catch (IncorrectCredentialsException e) {
- // password 错误
- e.printStackTrace();
- return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("password错误");
- }
- }
-
- /**
- * 退出登录
- * 销毁主体的认证记录(信息),下次访问需要重新认证
- *
- * @return
- */
- @RequestMapping("/logout")
- public ResponseEntity<String> logout() {
- Subject subject = SecurityUtils.getSubject();
-
- User user = (User) subject.getPrincipal();
- userService.logout(user.getUsername());
- subject.logout();
-
- return ResponseEntity.ok().build();
- }
- }
UserServiceImpl
- /**
- * @author passerbyYSQ
- * @create 2020-08-21 11:02
- */
- @Service("userService") // 不要忘了
- @Transactional // 开启事务。有需要再开启
- public class UserServiceImpl implements UserService {
-
- @Autowired
- private UserDAO userDAO;
-
- @Override
- public String generateJwt(String username) {
- // 8个字符的随机字符串,作为生成jwt的随机盐
- // 保证每次登录成功返回的Token都不一样
- String jwtSecret = RandomUtil.generateStr(8);
- // 将此次登录成功的jwt secret存到数据库,下次携带jwt时解密需要使用
- userDAO.updateJwtSecretByUsername(username, jwtSecret);
- return JwtUtil.generateJwt(username, jwtSecret);
- }
-
- @Override
- public User findByUsername(String username) {
- return userDAO.findByUsername(username);
- }
-
- @Override
- public void logout(String username) {
- // 将jwt secret置为空
- userDAO.updateJwtSecretByUsername(username, "");
- }
-
- }
LoginRealm:处理密码登录的Realm,复写doGetAuthenticationInfo()方法
- /**
- * @author passerbyYSQ
- * @create 2020-08-20 23:31
- */
- public class LoginRealm extends AuthorizingRealm {
-
- /*
- 如果@Autowired,需要在当前类前加上@Componet注解,将当前类的实例注入到IOC容器
- 但是如果有多个类似的类,都要注册到容器中,不太好。我可以新建一个管理类,注册到容器中,
- 为我们统一获取@Autowired的实例
- */
- // @Autowired
- // private UserService userService;
-
-
- /**
- * 或者在ShiroConfig中设置
- */
- public LoginRealm() {
- // 匹配器。需要与密码加密规则一致
- HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
- // 设置匹配器的加密算法
- hashedCredentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
- // 设置匹配器的哈希散列次数
- hashedCredentialsMatcher.setHashIterations(1024);
- // 将对应的匹配器设置到Realm中
- this.setCredentialsMatcher(hashedCredentialsMatcher);
- }
-
- /**
- * 可以往Shiro中注册多种Realm。某种Token对象需要对应的Realm来处理。
- * 复写该方法表示该方法支持处理哪一种Token
- * @param token
- * @return
- */
- @Override
- public boolean supports(AuthenticationToken token) {
- return token instanceof UsernamePasswordToken;
- }
-
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- return null;
- }
-
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
- // 从Token中获取身份信息。这里实际上是username,这里从UsernamePasswordToken的源码可以看出
- String principal = (String) token.getPrincipal();
- // 从IOC容器中获取UserService组件
- UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
-
- User user = userService.findByUsername(principal);
-
- if (!ObjectUtils.isEmpty(user)) {
- // 返回正确的信息(数据库存储的),作为比较的基准
- return new SimpleAuthenticationInfo(
- user, user.getPassword(),
- ByteSource.Util.bytes(user.getSalt()), this.getName()
- );
- }
-
- return null;
- }
- }
密码登录的主要流程基本就这么多。但是还需要写配置类将LoginRealm注册到Shiro中,这个后面统一把 Shiro 的配置类贴出来。上面代码还涉及到两个工具类:
ApplicationContextUtil:用于更加灵活地获取IOC容器中组件
JwtUtil:jwt的工具类,提供几个主要的静态方法,包括:生成jwt,校验jwt,获取jwt里面的数据,获取jwt的签发时间。
ApplicationContextUtil
- /**
- * @author passerbyYSQ
- * @create 2020-08-21 11:50
- */
- @Component // 加入容器
- public class ApplicationContextUtil implements ApplicationContextAware {
-
- // IOC容器
- private static ApplicationContext context;
-
-
- /**
- * 将IOC容器回调给我们,我们将它缓存起来
- *
- * @param applicationContext
- * @throws BeansException
- */
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- context = applicationContext;
- }
-
- /**
- * 从IOC容器中获取组件(bean)
- *
- * @param beanName
- * @return
- */
- public static Object getBean(String beanName) {
- return context.getBean(beanName);
- }
- }
JwtUtil
- /**
- * JWT的工具类,包括签发、验证、获取信息
- *
- * @author passerbyYSQ
- * @create 2020-08-22 11:13
- */
- public class JwtUtil {
-
- // 有效时间:7天
- private static final long EFFECTIVE_DURATION = 1000 * 60 * 60 * 24 * 7;
- // 发行者
- private static final String ISSUER = "net.ysq";
-
- /**
- * 生成Jwt字符串
- *
- * @param claims 由于类库只支持基本类型的包装类、String、Date,我们最好使用String
- * @param secret 加密的密钥
- * @return
- */
- public static String generateJwt(Map<String, String> claims, String secret) {
- // 发行时间
- Date issueAt = new Date();
- // 过期时间
- Date expireAt = new Date(issueAt.getTime() + EFFECTIVE_DURATION);
- // 加密算法
- Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));
-
- JWTCreator.Builder builder = JWT.create()
- .withIssuer(ISSUER)
- .withIssuedAt(issueAt)
- .withExpiresAt(expireAt);
-
- // 设置Payload信息
- Set<String> keySet = claims.keySet();
- for (String key : keySet) {
- builder.withClaim(key, claims.get(key));
- }
-
- return builder.sign(algorithm);
- }
-
- public static String generateJwt(String username, String secret) {
- Map<String, String> claims = new HashMap<>();
- claims.put("username", username);
- return generateJwt(claims, secret);
- }
-
- /**
- * 校验jwt是否合法
- *
- * @param jwt
- * @param claims
- * @return
- */
- public static boolean verifyJwt(String jwt, Map<String, String> claims, String secret) {
- // 解密算法
- Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));
- try {
- Verification verification = JWT.require(algorithm).withIssuer(ISSUER);
-
- Set<String> keySet = claims.keySet();
- for (String key : keySet) {
- verification.withClaim(key, claims.get(key));
- }
-
- JWTVerifier verifier = verification.build();
- verifier.verify(jwt);
-
- return true;
- } catch (IllegalArgumentException | JWTVerificationException e) {
- e.printStackTrace();
- }
- return false;
- }
-
- public static boolean verifyJwt(String jwt, String username, String secret) {
- Map<String, String> claims = new HashMap<>();
- claims.put("username", username);
- return verifyJwt(jwt, claims, secret);
- }
-
- /**
- * 根据key获取claim值
- *
- * @param jwt
- * @param key
- * @return
- */
- public static String getClaimByKey(String jwt, String key) {
- try {
- DecodedJWT decodedJwt = JWT.decode(jwt);
- return decodedJwt.getClaim(key).asString(); // 注意不要用toString
- } catch (JWTDecodeException e) {
- e.printStackTrace();
- }
- return null;
- }
-
- /**
- * 返回过期的时间
- *
- * @param jwt
- * @return
- */
- public static Date getExpireAt(String jwt) {
- try {
- DecodedJWT decodedJwt = JWT.decode(jwt);
- return decodedJwt.getExpiresAt();
- } catch (JWTDecodeException e) {
- e.printStackTrace();
- }
- return null;
- }
- }
JwtToken:类比UsernamePasswordToken
- /**
- * 或者直接实现AuthenticationToken也可以,不需要host
- *
- * @author passerbyYSQ
- * @create 2020-08-22 10:42
- */
- public class JwtToken implements HostAuthenticationToken {
-
- // JWT字符串
- private String token;
-
- private String host;
-
- public JwtToken(String token) {
- this(token, null);
- }
-
- public JwtToken(String token, String host) {
- this.token = token;
- this.host = host;
- }
-
- @Override
- public String getHost() {
- return token;
- }
-
- /**
- * 返回身份信息(相当于username),这个方法的返回比较重要,前面的代码也说到了
- * Jwt里面包含一个访问主体的身份(比如说username)
- * @return
- */
- @Override
- public Object getPrincipal() {
- return token;
- }
-
- /**
- * 返回凭证信息(相当于password)
- * Jwt本身就是一个令牌凭证,在服务端通过解密校验
- * @return
- */
- @Override
- public Object getCredentials() {
- return token;
- }
-
- public String getToken() {
- return token;
- }
-
- public void setToken(String token) {
- this.token = token;
- }
-
- public void setHost(String host) {
- this.host = host;
- }
- }
JwtAuthenticatingFilter:全局请求的过滤器。驳回没有携带token的请求,不能访问受限资源。对于携带token的请求,校验token是否有效,有效最重放行到controller层
- /**
- * @author passerbyYSQ
- * @create 2020-08-22 12:06
- */
- public class JwtAuthenticatingFilter extends BasicHttpAuthenticationFilter {
-
- // 是否刷新token
- private boolean shouldRefreshToken;
-
- public JwtAuthenticatingFilter() {
- this.shouldRefreshToken = false;
- }
-
- /**
- * 请求是否允许放行
- * 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。
- */
- @Override
- protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
- boolean allowed = false;
- try {
- allowed = executeLogin(request, response);
- } catch(IllegalStateException e){ //not found any token
- System.out.println("Not found any token");
- }catch (Exception e) {
- System.out.println("Error occurs when login");
- }
- return allowed || super.isPermissive(mappedValue);
- }
-
- /**
- * 父类executeLogin()首先会createToken(),然后调用shiro的Subject.login()方法。
- *
- * executeLogin()的逻辑是不是跟UserController里面的密码登录逻辑很像?
- *
- * @param request
- * @param response
- * @return
- */
- @Override
- protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- // 从请求头中的Authorization字段尝试获取jwt token
- String token = httpRequest.getHeader("Authorization");
- if (StringUtils.isEmpty(token)) {
- // 从请求头中的token字段(自定义字段)尝试获取jwt token
- token = httpRequest.getHeader("token");
- }
- if (StringUtils.isEmpty(token)) {
- // 从url参数中尝试获取jwt token
- token = httpRequest.getParameter("token");
- }
-
- if (!StringUtils.isEmpty(token)) {
- return new JwtToken(token);
- }
-
- return null;
- }
-
- /**
- * 如果这个Filter在之前isAccessAllowed()方法中返回false,则会进入这个方法。我们这里直接返回错误的response
- * @param request
- * @param response
- * @return
- * @throws Exception
- */
- @Override
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
- HttpServletResponse httpResponse = WebUtils.toHttp(response);
- httpResponse.setCharacterEncoding("UTF-8");
- httpResponse.setContentType("application/json;charset=UTF-8");
- httpResponse.setStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION.value());
- PrintWriter writer = response.getWriter();
- writer.print("无效token");
- fillCorsHeader(request, httpResponse);
- return false;
- }
-
- /**
- * 登录成功后判断是否需要刷新token
- * 登录成功说明:jwt有效,尚未过期。当离过期时间不足一天时,往响应头中放入新的token返回给前端
- *
- * @param token
- * @param subject
- * @param request
- * @param response
- * @return
- */
- @Override
- protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
- ServletRequest request, ServletResponse response) {
-
- String oldToken = (String) token.getPrincipal();
-
- Date expireAt = JwtUtil.getExpireAt(oldToken);
- int countDownDays = (int) DateTimeUtil.differDaysBetween(
- LocalDateTime.now(), DateTimeUtil.toLocalDateTime(expireAt));
-
- if (shouldRefreshToken && !ObjectUtils.isEmpty(expireAt)
- && countDownDays < 1) { // 如果离过期时间不足一天
-
- UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
- User user = (User) subject.getPrincipal();
- String newToken = userService.generateJwt(user.getUsername());
- HttpServletResponse httpResponse = (HttpServletResponse) response;
- httpResponse.addHeader("token", newToken);
- }
-
- return true;
- }
-
- /**
- * 添加跨域支持
- * @param request
- * @param response
- * @throws Exception
- */
- @Override
- protected void postHandle(ServletRequest request, ServletResponse response) {
- fillCorsHeader(request, response);
- }
-
- /**
- * 设置跨域
- */
- public void fillCorsHeader(ServletRequest request, ServletResponse response) {
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- HttpServletResponse httpResponse = (HttpServletResponse) response;
- httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin"));
- httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
- httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
- // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
- if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
- httpResponse.setStatus(HttpStatus.OK.value());
- }
- }
-
- public boolean isShouldRefreshToken() {
- return shouldRefreshToken;
- }
-
- public void setShouldRefreshToken(boolean shouldRefreshToken) {
- this.shouldRefreshToken = shouldRefreshToken;
- }
- }
JwtRealm
- /**
- * @author passerbyYSQ
- * @create 2020-08-23 18:24
- */
- public class JwtRealm extends AuthorizingRealm {
-
- public JwtRealm() {
- // 用我们自定的Matcher
- this.setCredentialsMatcher(new JwtCredentialsMatcher());
- }
-
- @Override
- public boolean supports(AuthenticationToken token) {
- return token instanceof JwtToken;
- }
-
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- return null;
- }
-
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
- // JwtToken jwtToken = (JwtToken) token;
- // String tokenStr = jwtToken.getToken();
-
- // 取决于JwtToken的getPrincipal()
- String tokenStr = (String) token.getPrincipal();
-
- // 从jwt字符串中解析出username信息
- String username = JwtUtil.getClaimByKey(tokenStr, "username");
- if (!Strings.isEmpty(username)) {
- UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
- // 根据token中的username去数据库核对信息,返回用户信息,并封装称SimpleAuthenticationInfo给Matcher去校验
- User user = userService.findByUsername(username);
- // principle是身份信息,简单的可以放username,也可以将User对象作为身份信息
- // 身份信息可以在登录成功之后通过subject.getPrinciple()取出
- return new SimpleAuthenticationInfo(user, user.getJwtSecret(), this.getName());
- }
-
- return null;
- }
- }
JwtCredentialsMatcher:Jwt的比较器
- /**
- * @author passerbyYSQ
- * @create 2020-08-23 18:42
- */
- public class JwtCredentialsMatcher implements CredentialsMatcher {
- @Override
- public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
- // AuthenticationInfo info 是我们在JwtRealm中doGetAuthenticationInfo()返回的那个
- User user = (User) info.getPrincipals().getPrimaryPrincipal();
- String secret = (String) info.getCredentials();
-
- // String tokenStr = ((JwtToken) token).getToken();
- String tokenStr = (String) token.getPrincipal();
-
- // 校验jwt有效
- return JwtUtil.verifyJwt(tokenStr, user.getUsername(), secret);
- }
- }
总结来说,这个配置类主要干两种事情:
1、将我们上述定义的Realm,过滤器等,注册给Shiro
2、设置请求的拦截规则
代码解析细节,参看注释。个人强迫症,注释写的还算是比较可以的。
- /**
- * 整合Shiro框架的配置类
- *
- * @author passerbyYSQ
- * @create 2020-08-20 23:10
- */
- @Configuration
- public class ShiroConfig {
-
- @Bean
- public ShiroFilterFactoryBean shiroFilterFactoryBean(
- DefaultWebSecurityManager securityManager, ShiroFilterChainDefinition chainDefinition) {
- ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
-
- // 必须的设置。我们自定义的Realm此时已经被设置到securityManager中了
- factoryBean.setSecurityManager(securityManager);
-
- // 注册我们写的过滤器
- Map<String, Filter> filters = factoryBean.getFilters();
- filters.put("jwtAuth", new JwtAuthenticatingFilter());
-
- factoryBean.setFilters(filters);
-
- // 设置请求的过滤规则。其中过滤规则中用到了我们注册的过滤器:jwtAuth
- factoryBean.setFilterChainDefinitionMap(chainDefinition.getFilterChainMap());
-
- return factoryBean;
- }
-
- @Bean
- public DefaultWebSecurityManager securityManager(Authenticator authenticator) {
- DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
- // 所有的Realm都用这个全局缓存。不生效,需要在realm中设置缓存。原因暂时搞不懂。
- // securityManager.setCacheManager(new EhCacheManager());
- securityManager.setAuthenticator(authenticator);
- return securityManager;
- }
-
- /**
- * 设置请求的过滤规则
- * @return
- */
- @Bean
- public ShiroFilterChainDefinition shiroFilterChainDefinition() {
- DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
- chainDefinition.addPathDefinition("/user/register", "noSessionCreation,anon");
- chainDefinition.addPathDefinition("/user/login", "noSessionCreation,anon"); //login不做认证,noSessionCreation的作用是用户在操作session时会抛异常
-
- // 注意第2个参数的"jwtAuth"需要与上面的 filters.put("jwtAuth", new JwtAuthenticatingFilter()); 一致
- chainDefinition.addPathDefinition("/user/logout", "noSessionCreation,jwtAuth[permissive]"); //做用户认证,permissive参数的作用是当token无效时也允许请求访问,不会返回鉴权未通过的错误
- chainDefinition.addPathDefinition("/**", "noSessionCreation,jwtAuth"); // 默认进行用户鉴权
- return chainDefinition;
- }
-
- /**
- * 初始化Authenticator,将我们需要的Realm设置进去
- * Shiro会将Authenticator设置到SecurityManager里面
- */
- @Bean
- public Authenticator authenticator(@Qualifier("loginRealm") Realm loginRealm, @Qualifier("jwtRealm") Realm jwtRealm) {
-
- ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
- //设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
- authenticator.setRealms(Arrays.asList(loginRealm, jwtRealm));
- //设置多个realm认证策略,一个成功即跳过其它的
- authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
- return authenticator;
- }
-
-
- /**
- * 返回我们自定义的Realm
- *
- * @return
- */
- @Bean("loginRealm") // 自动配置类中有同名组件,如果只写@Bean,会出现歧义
- public Realm loginRealm(EhCacheManager ehCacheManager) {
- LoginRealm loginRealm = new LoginRealm();
-
- // AuthenticatingRealm里面的isAuthenticationCachingEnabled()
- loginRealm.setCacheManager(ehCacheManager);
- loginRealm.setCachingEnabled(true); // 这句话不能少!!!
- loginRealm.setAuthenticationCachingEnabled(true); // 认证缓存
- loginRealm.setAuthorizationCachingEnabled(true); // 授权缓存
-
- return loginRealm;
- }
-
- @Bean("jwtRealm")
- public Realm jwtRealm(EhCacheManager ehCacheManager) {
- JwtRealm jwtRealm = new JwtRealm();
-
- jwtRealm.setCacheManager(ehCacheManager);
- jwtRealm.setCachingEnabled(true); // 这句话不能少!!!
- jwtRealm.setAuthenticationCachingEnabled(true); // 认证缓存
- jwtRealm.setAuthorizationCachingEnabled(true); // 授权缓存
-
- return jwtRealm;
- }
-
- /**
- * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
- * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,
- * 如果要完全禁用,要配合上过滤规则的noSessionCreation的Filter来实现
- */
- @Bean
- protected SessionStorageEvaluator sessionStorageEvaluator(){
- DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
- sessionStorageEvaluator.setSessionStorageEnabled(false);
- return sessionStorageEvaluator;
- }
-
- /**
- * shiro的全局缓存管理器
- * @return
- */
- @Bean
- public EhCacheManager ehCacheManager() {
- return new EhCacheManager();
- }
-
- }
博客注重思路逻辑分析。有需要的同学可以去Github自取源码。GitHub - passerbyYSQ/SpringBoot_Shiro_Jwt
求赞和收藏。。。么么哒
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。