Springboot+jwt+shiro
程序员文章站
2024-03-20 20:09:22
...
一. Jwt配置
1、JwtUtils
public class JwtUtils {
public static final String SECRET = "f2ea985e640aae55392cfe2e1ded562c";
public static final Integer EXPIRE = 1000*60*60*2;
public static final String HEADER = "jsbc-auth-token";
/**
* 生成 token
*
* @param username 用户名
* @return 加密的token
*/
public static String createToken(String username) {
try {
Date expireDate = new Date(System.currentTimeMillis()+EXPIRE);
return JWT
.create()
.withClaim("username",username)
//到期时间
.withExpiresAt(expireDate)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(Algorithm.HMAC256(getKey()));
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* 校验 token 是否正确
*
* @param token **
* @param username 用户名
* @return 是否正确
*/
public static boolean verify(String token, String username) {
try {
//在token中附带了username信息
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(getKey()))
.withClaim("username", username)
.build();
//验证 token
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;
}
}
/**
* 根据给定的字节数组使用AES加密算法构造一个**
* @return
*/
private static String getKey() {
byte[] encodedKey = Base64.decodeBase64(SECRET);
// 根据给定的字节数组使用AES加密算法构造一个**
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key.toString();
}
}
2. JwtToken(重写AuthenticationToken)
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;
}
}
3. JwtFilter(嵌入在Shiro中)
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
//token 错误
//responseError(response, e.getMessage());
throw new LoginException(e.getMessage());
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return true;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(JwtUtils.HEADER);
return token != null;
}
/**
* 执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(JwtUtils.HEADER);
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
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);
}
/**
* 将非法请求跳转到 /unauthorized/**
*/
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/unauthorized/" + message);
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
二. Shiro配置
1、ShiroRealm(认证和授权)
@Component
public class ShiroRealm extends AuthorizingRealm {
@Autowired
SysUserService sysUserService;
/**
* 授权(取出Session中的User对象)
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//session中获取用户
String username = JwtUtils.getUsername(principalCollection.toString());
//DB获取用户的密码
SysUserModel user = sysUserService.findUserByName(username);
//权限集合
List<String> permissionList = new ArrayList<>();
//角色集合
List<String> roleNameList = new ArrayList<>();
Set<SysRoleModel> roleSet = user.getRoles();
if(CollectionUtils.isNotEmpty(roleSet)){
for(SysRoleModel role : roleSet){
roleNameList.add(role.getRname());
Set<SysPermissionModel> permissionSet = role.getPermissions();
if(CollectionUtils.isNotEmpty(permissionSet)){
for(SysPermissionModel permission : permissionSet){
//放入权限集合
permissionList.add(permission.getPname());
}
}
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissionList);
info.addRoles(roleNameList);
return info;
}
/**
* 认证登陆(认证登陆成功后把User对应放入session中)
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
String token = (String) authenticationToken.getCredentials();
// 解密获得username,用于和数据库进行对比
String username = JwtUtils.getUsername(token);
//DB获取用户的密码
SysUserModel sysUserModel = sysUserService.findUserByName(username);
if (username == null || ObjectUtils.isEmpty(sysUserModel) || !JwtUtils.verify(token, username)) {
throw new AuthenticationException(LoginEnum.TOKEN_AUTH_FAIL.getMessage());
}
return new SimpleAuthenticationInfo(token, token, ByteSource.Util.bytes(sysUserModel.getCredentialsSalt()),this.getClass().getName());
}
/**
* 必须重写此方法,不然会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
}
2、ShiroConfig(配置ShiroFilter,并把JwtFilter注入)
@Configuration
public class ShiroConfig {
/**
* 项目启动shiroFilter首先会被初始化,并且逐层传入SecurityManager,Realm,matcher
* @param manager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
bean.setSecurityManager(manager);
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>();
//设置我们自定义的JWT过滤器
filterMap.put("jwt", new JwtFilter());
bean.setFilters(filterMap);
//登陆界面
bean.setLoginUrl("/login");
//成功后页面
bean.setSuccessUrl("/index");
//无权限后的页面
bean.setUnauthorizedUrl("/unauthorized");
//键值对:请求-拦截器(权限配置)
LinkedHashMap<String,String> filterChainDefinitonMap = new LinkedHashMap<>();
/**不做身份验证*/
//登陆不需要任何过滤
filterChainDefinitonMap.put("/login","anon");
filterChainDefinitonMap.put("/doLogin","anon");
//注册
filterChainDefinitonMap.put("/register","anon");
//druid
filterChainDefinitonMap.put("/druid/**","anon");
//swagger
filterChainDefinitonMap.put("/swagger-ui.html", "anon");
filterChainDefinitonMap.put("/swagger-resources/**", "anon");
filterChainDefinitonMap.put("/v2/**", "anon");
filterChainDefinitonMap.put("/webjars/**", "anon");
/**需要身份验证*/
//anon 无参,开放权限,可以理解为匿名用户或游客
//authc 无参,需要认证
//logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
//authcBasic 无参,表示 httpBasic 认证
//user 无参,表示必须存在用户,当登入操作时不做检查
//ssl 无参,表示安全的URL请求,协议为 https
//perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过
//roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
//rest[user] 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
//port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的
// 所有请求通过我们自己的JWT Filter
filterChainDefinitonMap.put("/**", "jwt");
// 访问 /unauthorized/** 不通过JWTFilter
filterChainDefinitonMap.put("/unauthorized/**", "anon");
//其他请求只验证是否登陆过
//filterChainDefinitonMap.put("/**","authc");
//首页地址index,使用authc过滤器进行处理
filterChainDefinitonMap.put("/index","authc");
//只有角色中拥有admin才能访问admin
filterChainDefinitonMap.put("/admin","roles[admin]");
//拥有edit权限
filterChainDefinitonMap.put("/edit","perms[Update]");
//放入Shiro过滤器
bean.setFilterChainDefinitionMap(filterChainDefinitonMap);
return bean;
}
/**
* 将定义好的Realm放入安全会话中心
* @param shiroRealm
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(shiroRealm);
/*
* 关闭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;
}
/**
* 将自定义的校验规格放入Realm
* @param matcher
* @return
*/
// @Bean("shiroRealm")
// public ShiroRealm shiroRealm(@Qualifier("credentialmatcher") Credentialmatcher matcher){
// ShiroRealm shiroRealm = new ShiroRealm();
// //信息放入缓存
// shiroRealm.setCacheManager(new MemoryConstrainedCacheManager());
// shiroRealm.setCredentialsMatcher(matcher);
// return shiroRealm;
// }
/**
* 校验规则
* @return 校验实例
*/
// @Bean("credentialmatcher")
// public Credentialmatcher credentialmatcher(){
// return new Credentialmatcher();
// }
/**
* Spring与Shiro关联
* @param manager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager manager){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(manager);
return advisor;
}
/**
* 开启代理
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
3、登录测试类
@Controller
@RequestMapping
public class HomeController {
@Autowired
SysUserService sysUserService;
@Autowired
PasswordHelper passwordHelper;
@GetMapping("login")
public Object login() {
return "pages/login";
}
@GetMapping("logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
//用户不为空,则手动登出
if(subject !=null){
subject.logout();
}
return "pages/login";
}
@GetMapping("unauthorized")
public Object unauthc() {
return "pages/unauthorized";
}
@GetMapping("index")
public Object index() {
return "pages/index";
}
@GetMapping("admin")
@ResponseBody
@RequiresRoles("admin")
public Object admin() {
return "admin success";
}
@GetMapping("edit")
@ResponseBody
@RequiresPermissions("Update")
public Object edit() {
return "edit success";
}
@GetMapping("removeable")
@ResponseBody
@RequiresPermissions("Delete")
public Object removeable() {
return "removeable success";
}
// @PostMapping("doLogin")
// public Object doLogin(@RequestParam String username,@RequestParam String password) {
// UsernamePasswordToken token = new UsernamePasswordToken(username,password);
// Subject subject = SecurityUtils.getSubject();
// try {
// subject.login(token);
//
// SysUserModel sysUserModel = (SysUserModel) subject.getPrincipal();
// subject.getSession().setAttribute("user",sysUserModel);
// } catch (IncorrectCredentialsException ice) {
// return "login";
// } catch (UnknownAccountException uae) {
// return "login";
// }
//
// return "index";
// }
@PostMapping("doLogin")
@ResponseBody
public Object doLogin(@RequestParam String username,@RequestParam String password) {
SysUserModel sysUserModel = sysUserService.findUserByName(username);
if(ObjectUtils.isEmpty(sysUserModel)) {
throw new LoginException(LoginEnum.USERNAME_ERROR.getMessage());
} else if(!sysUserModel.getPassword().equals(passwordHelper.encryptPassword(password,sysUserModel.getCredentialsSalt()))) {
throw new LoginException(LoginEnum.PASSWORD_ERROR.getMessage());
}else {
return ReturnVOUtils.success(JwtUtils.createToken(username));
}
}
// @PostMapping("register")
@GetMapping("register")
@ResponseBody
public Object register(@RequestParam String username,@RequestParam String password) {
SysUser sysUser = new SysUser();
sysUser.setUsername(username);
sysUser.setPassword(password);
passwordHelper.encryptPassword(sysUser);
SysRole sysRole = new SysRole();
sysRole.setTsrId(1);
List<SysRole> roles = new ArrayList<>();
roles.add(sysRole);
sysUser.setRoles(roles);
sysUserService.save(sysUser);
return "SUCCESS";
}
}
4、PasswordHelper
@Component
public class PasswordHelper {
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
/**
* 基础散列算法
*/
public static final String ALGORITHM_NAME = "md5";
/**
* 自定义散列次数
*/
public static final int HASH_ITERATIONS = 2;
public void encryptPassword(SysUser sysUser) {
// User对象包含最基本的字段Username和Password
sysUser.setSalt(randomNumberGenerator.nextBytes().toHex());
// 将用户的注册密码经过散列算法替换成一个不可逆的新密码保存进数据,散列过程使用了盐
String newPassword = new SimpleHash(ALGORITHM_NAME,sysUser.getPassword(), ByteSource.Util.bytes(sysUser.getCredentialsSalt()),HASH_ITERATIONS).toHex();
sysUser.setPassword(newPassword);
}
public String encryptPassword(String password,String credentialsSalt) {
return new SimpleHash(ALGORITHM_NAME,password, ByteSource.Util.bytes(credentialsSalt),HASH_ITERATIONS).toHex();
}
}
三、自定义异常
1、LoginException
public class LoginException extends RuntimeException {
public LoginException(String message) {
super(message);
}
}
2、ExceptionHandle
@ControllerAdvice
public class ExceptionHandle {
@ExceptionHandler(LoginException.class)
public String loginException(Exception e, HttpServletRequest request) {
Map<String,Object> map = new HashMap<>();
//传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
request.setAttribute("javax.servlet.error.status_code",500);
map.put("module","登录模块异常");
map.put("message",e.getMessage());
request.setAttribute("nzd",map);
request.setAttribute("message",e.getMessage());
//转发到 /error
return "forward:/error";
}
@ExceptionHandler(ShiroException.class)
public String shiroException(Exception e, HttpServletRequest request) {
Map<String,Object> map = new HashMap<>();
//传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
request.setAttribute("javax.servlet.error.status_code",500);
map.put("module","登录模块异常");
map.put("message","没有权限访问");
request.setAttribute("nzd",map);
request.setAttribute("message","没有权限访问");
//转发到 /error
return "forward:/error";
}
}
3、ErrorAttributes
@Component
public class ErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributesMap = super.getErrorAttributes(webRequest, includeStackTrace);
errorAttributesMap.replace("timestamp", DateUtils.formatDateTime((Date)errorAttributesMap.get("timestamp")));
errorAttributesMap.put("company","jsbc");
Map<String,Object> nzd = (Map<String,Object>) webRequest.getAttribute("nzd", 0);
errorAttributesMap.put("nzd",nzd);
errorAttributesMap.put("success",false);
errorAttributesMap.put("code",errorAttributesMap.get("status"));
errorAttributesMap.remove("trace");
return errorAttributesMap;
}
}