本文共 13275 字,大约阅读时间需要 44 分钟。
spring-boot 整合shiro和jwt的认证方式
前言:shiro框架是如今web开发做权限控制非常流行的,JWT就是(JSON Web Token),即如何存储认证后的信息的一种方式,比如以前登陆后,判断username,password如果ok,就存储根据username找出的userinfo到session中。用户是否登陆,判断session中是否有userinfo。而JWT就是把认证后的信息采用一种格式token,明文存储在客户端,客服端每次请求带上这个token,可以判断这个token是否是我服务端颁发的。
一、jwt的基本演示
/** * JWT简易工具类 */public class JWTUtil { private static final Logger log = LoggerFactory.getLogger(JWTUtil.class); /** * 过期时间5分钟 */ private static final long EXPIRE_TIME = 5 * 60 * 1000; /** * 生成签名,5min后过期 * @param username 用户名 * @param secret 用户的密码 * @return 加密的token */ public static String sign(String username, String secret) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm); } /** * 校验token是否正确 * @param token 密钥 * @param secret 用户的密码 * @return 是否正确 */ public static boolean verify(String token, String username, String secret) { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build(); try { DecodedJWT jwt = verifier.verify(token); } catch (JWTVerificationException e) { return false; } return true; } /** * 获得token中的信息:无需secret解密也能获得 */ public static String getUsername(String token) { return getClaim(token,"username"); } public static String getClaim(String token,String key) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim(key).asString(); } public static void main(String[] args) { String username = "myUsername"; String secret = "123456"; String token = sign(username, secret); // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDk2NjYxNjMsInVzZXJuYW1lIjoibXlVc2VybmFtZSJ9._uOb80rq2XkhtzLDR3mqF-2TZt_Y1zcVVg79loLZOzM System.out.println(token); boolean verify = verify(token, username, secret); System.out.println(verify); // true String usernames = getUsername(token); System.out.println(usernames); // myUsername }}
上面主要有三个方法,sign()方法生成token,verify(token,username,secret)方法用于校验token是否有效 , getClaim()及其类似的方法是用来获取存储的信息。
二、web项目演示
1、项目结构
2、pom.xml
com.auth0 java-jwt 3.4.0 org.apache.shiro shiro-spring 1.4.0 slf4j-api org.slf4j org.apache.shiro shiro-ehcache 1.4.0 slf4j-api org.slf4j
3、pojo、service等辅助类
public class User { private Integer id; private String username; private String name; private String password; private String salt; private String phone; private String tips; private Integer state; private Date createdTime; private Date updatedTime;
public class Role { private Integer id; private String role; private String description; private Date createdTime; private Date updatedTime;
public class UserInfo extends User{ private Listroles = new ArrayList<>(); public List getRoles() { return roles; } public void setRoles(List roles) { this.roles = roles; } }
@Servicepublic class UserService { static Listusers = new ArrayList<>(); { users.add("admin"); users.add("guest"); users.add("admin99"); } /** * 根据用户名查询用户信息,没有找到返回null,找到返回全信息 */ public UserInfo findByUsername(String username) { if (users.contains(username)) { UserInfo userinfo = new UserInfo(); userinfo.setUsername(username); userinfo.setState(1); userinfo.setCreatedTime(new Date()); userinfo.setName("系统管理员"); userinfo.setSalt("ef748186673033723bbf4e056f4ec92e"); userinfo.setPassword("331be47d5bc559cc9a32a86a04e5ef32"); List roles = new ArrayList<>(); Role role = new Role(); role.setId(1); role.setRole("admin"); role.setDescription("超级管理员"); roles.add(role); userinfo.setRoles(roles); return userinfo; } return null; }}
4、 主要流程配置等文字描述:
1) 流程:创建一个login登陆接口,用做用户登陆时创建token,并且手动保存这个token,另外创建一个/api/v1/hello接口,使用自己手动保存的token进行认证。在这个流程中,
登陆就是生成token的过程,并且返回到前端保存,那么每次需要jwt校验权限的路径,我们需要配置一个拦截器。
2) 拦截器:这个拦截器基于shiro的BasicHttpAuthenticationFilter拦截器,主要功能就是取出header头Authorization的值,执行 getSubject(request, response).login(token);操作,
而login操作我们知道,最终调用的realm中的doGetAuthenticationInfo方法
3)自定义的realm:对token中提取username进行查询等操作,并且校验token的合法性,如果没有问题,就返回SimpleAuthenticationInfo(token,tokenStr., this)。
5、核心配置类
1) JWTToken
/** * jwttoken只有一个字段token,并且getPrincipal和getCredentials都是得到这一个字段的值。 */public class JWTToken implements AuthenticationToken { private static final long serialVersionUID = -329196857201301194L; /** * jwt加密token */ private String token; public JWTToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; }}
2、 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) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String authorization = httpServletRequest.getHeader("Authorization"); JWTToken token = new JWTToken(authorization); getSubject(request, response).login(token); return true; } /** * 如果有Authorization字段的都需自动执行登陆操作,否则返回true,表示进入后面流程。 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (isLoginAttempt(request, response)) { return executeLogin(request, response); } return true; } /** * 将非法请求 /401 */ private void response401(ServletRequest req, ServletResponse resp) { try { HttpServletResponse httpServletResponse = (HttpServletResponse) resp; httpServletResponse.sendRedirect("/401"); } catch (IOException e) { LOGGER.error(e.getMessage()); } }}
3 、myRealm
public class MyRealm extends AuthorizingRealm { private static final Logger _logger = LoggerFactory.getLogger(MyRealm.class); @Override public boolean supports(AuthenticationToken token) { return token instanceof AuthenticationToken; } @Autowired UserService userService; /** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = JWTUtil.getUsername(principals.toString()); UserInfo userinfo = userService.findByUsername(username); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); if(userinfo==null) {return authorizationInfo;} for (Role role : userinfo.getRoles()) { authorizationInfo.addRole(role.getRole()); } return authorizationInfo; } /** * 认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String tokenStr = (String) token.getCredentials(); String username = JWTUtil.getUsername(tokenStr); if (username == null) { throw new AuthenticationException("token invalid"); } UserInfo userinfo = userService.findByUsername(username); if (userinfo == null) { throw new AuthenticationException("用户不存在!"); } if (!JWTUtil.verify(tokenStr, username, userinfo.getPassword())) { throw new AuthenticationException("Token认证失败"); } // 这里第二个参数是密码,如果没有配置凭证匹配算法,那么默认就是对getCredentials()和tokenStr直接对比,而二个又是一致的。 return new SimpleAuthenticationInfo(token, tokenStr, "my_realm"); }}
4、shiroConfig
@Configurationpublic class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 验证码过滤器 MapfiltersMap = shiroFilterFactoryBean.getFilters(); filtersMap.put("jwt", new JWTFilter()); shiroFilterFactoryBean.setFilters(filtersMap); Map filterChainDefinitionMap = new LinkedHashMap (); // 其他的全部进行jwt拦截器进行处理 filterChainDefinitionMap.put("/**", "jwt"); // 访问401和404页面不通过我们的Filter filterChainDefinitionMap.put("/401", "anon"); filterChainDefinitionMap.put("/404", "anon"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置realm. securityManager.setRealm(myShiroRealm()); //注入缓存管理器 securityManager.setCacheManager(ehCacheManager()); /* 关闭shiro自带的session */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; } //自定义Realm @Bean public MyRealm myShiroRealm() { MyRealm myShiroRealm = new MyRealm(); return myShiroRealm; } //加入注解的使用,可使用aop切面 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } //生命周期 @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public EhCacheManager ehCacheManager() { EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml"); return cacheManager; } }
6、controller
1)、登陆类LoginController,这个类有个测试方法直接生成可以使用的token
/** * 登录接口类 */@RestControllerpublic class LoginController { @Resource private UserService userService; private static final Logger _logger = LoggerFactory.getLogger(LoginController.class); @PostMapping("/login") public BaseResponselogin(User user) { _logger.info("用户请求登录获取Token"); String username = user.getUsername(); // admin String password = user.getPassword(); // 123456 UserInfo userinfo = userService.findByUsername(username); //随机数盐 String salt = userinfo.getSalt(); //原密码加密(通过username + salt作为盐) String encodedPassword = ShiroUtil.md5(password, salt); if (userinfo.getPassword().equals(encodedPassword)) { String token = JWTUtil.sign(username, encodedPassword); //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDk2NzMyMzMsInVzZXJuYW1lIjoiYWRtaW4ifQ.L_8-WmXOSk1_wZvZiOq2OkgTMgbPEIOAz7RVT47iqK8 return new BaseResponse<>(true, "Login success", token); } else { throw new UnauthorizedException(); } } /** * 这个方法用来快速生成登陆的token。 */ @Test public void test() { String username = "admin"; // admin String password = "123456"; // 123456 UserInfo userinfo = new UserService().findByUsername(username); String salt = userinfo.getSalt(); String encodedPassword = ShiroUtil.md5(password, salt); if (userinfo.getPassword().equals(encodedPassword)) { String token = JWTUtil.sign(username, encodedPassword); System.out.println(token); } } }
2)PublicController Api接口类。(这里注意,我们在jwt拦截器是全部放行的,但是如果没带header头,即没有登陆的情况下,使用了@RequiresAuthentication注解控制权限。)
/** * API接口类 */@RestController@RequestMapping(value = "/api/v1")public class PublicController { private static final Logger _logger = LoggerFactory.getLogger(PublicController.class); /** * hello接口 */ @RequestMapping(value = "/hello", method = RequestMethod.GET) @RequiresAuthentication public BaseResponse
三、测试
1、调用login接口中的test方法,得到下面token。(使用效果和login登陆一样)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDk2ODcxNzcsInVzZXJuYW1lIjoiYWRtaW4ifQ.60MAVxKU3iG8KS0ccYZExjhuJM0-9U2cS29RcklmPlE
2、我再用apiPost发送请求
转载地址:http://hhuni.baihongyu.com/