说明:前后端不分离的时候springboot+shiro可以实现有状态服务,前后端分离后工程就变成无状态服务,本文直接代码解决工程无状态问题。
注:
1.了解jwt的使用
2.文章的异常为自定义异常,粘贴代码的时候可以改为runtime异常!
3.文章中的重要内容已标红
一:前期准备工作
引入maven
io.jsonwebtoken
jjwt
0.9.1
com.auth0
java-jwt
3.4.0
org.apache.shiro
shiro-spring
1.3.2
com.github.theborakompanioni
thymeleaf-extras-shiro
1.2.1
二:TokenUtils工具类(其实就是jwt)
@Component
public class TokenUtil {
public static String key = "this is a jwt project";
//public static long ttlMillis = 5000;//设置过期时间
public static long ttlMillis = 30*60*100000;//设置过期时间
/**
* 用户登录成功后生成Jwt
* 使用Hs256算法 私匙使用用户mima
*
* @param user 登录成功的user对象
* @return
*/
public static String createJWT(User user) {
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
Map claims = new HashMap();
claims.put("user",user);
claims.put("userName",user.getUserName());
claims.put("passWord",user.getPassWord());
/*claims.put("roleList",user.getRoleList() == null || user.getRoleList().size() == 0 ? new ArrayList():user.getRoleList());
claims.put("resList",user.getResList() == null || user.getResList().size() == 0 ? new ArrayList() :user.getResList());*/
if(user.getResList() != null || user.getResList().size() > 0 ){
StringBuffer sb = new StringBuffer();
for (int a = 0; a< user.getResList().size(); a++){
if(a != user.getResList().size()-1){
sb.append(user.getResList().get(a).getResCode()).append(",");
} else {
sb.append(user.getResList().get(a).getResCode());
}
}
claims.put("resList",sb.toString());
}
//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
//生成签发人
String subject = user.getUserName();
//下面就是在为payload添加各种标准声明和私有声明了
//这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(UUID.randomUUID().toString())
//iat: jwt的签发时间
.setIssuedAt(now)
//代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.setSubject(subject)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, key);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* Token的jiemi
* @param token 加密后的token
* @param
* @return
*/
public static Claims parseJWT(String token) {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(key)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 校验token
* 在这里可以使用官方的校验,我这里校验的是token中携带的mima于数据库一致的话就校验通过
* @param token
* @return
*/
public static Boolean isVerify(String token) {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(key)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
if ((System.currentTimeMillis()-claims.getIssuedAt().getTime())
三:自定义拦截器(取代原来shiro的拦截校验规则)
/**
* @author 李庆伟
* @date 2020/7/11 13:57
*/
@Slf4j
public class CustomAuthorizationFilter extends BasicHttpAuthenticationFilter {
private static final String TOKEN = "Authentication";
/**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(TOKEN);
return authorization != null;
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader(TOKEN);
JwtToken token = new JwtToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 这里我们详细说明下为什么最终返回的都是true,即允许访问
* 例如我们提供一个地址 GET /article
* 登入用户和游客看到的内容是不同的
* 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
response401(request, response);
}
}
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);
}
/**
* 将非法请求跳转到 /401
*/
private void response401(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
四:重写原有的
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,passWord);
/**
* @author 李庆伟
* @date 2020/7/11 14:33
*/
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1282057025599826155L;
private String token;
private String exipreAt;
public JwtToken(String token) {
this.token = token;
}
public JwtToken(String token, String exipreAt) {
this.token = token;
this.exipreAt = exipreAt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
五:shiro的配置类
/**
* @author 李庆伟
* @date 2020/4/23 10:54
*/
@Configuration
public class ShiroConfiguration {
//不加这个注解不生效,具体不详
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
//将自己的验证方式加入容器
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
//权限管理,配置主要是Realm的管理认证
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filters = new HashMap<>();
//添加自定义过滤器
filters.put("jwt", new CustomAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filters);
//登录
shiroFilterFactoryBean.setLoginUrl("/user/login");
Map map = new LinkedHashMap();
map.put("/swagger-ui.html", "anon");//swagger
map.put("/webjars/**", "anon");
map.put("/v2/**", "anon");
map.put("/swagger-resources/**", "anon");//swagger
map.put("/**","jwt");//对所有用户认证
//map.put("/**/**", "anon");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//加入注解的使用,不加入这个注解不生效
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
六:Realm授权认证
/**
* @author 李庆伟
* @date 2020/4/23 10:55
*/
public class MyShiroRealm extends AuthorizingRealm {
//用于用户查询
@Autowired
private UserService userService;
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//角色权限和对应权限添加
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取登录用户名
//User user = (User) principals.getPrimaryPrincipal();
String token = principals.toString();
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
if(StringUtils.isEmpty(token)){ //如果用户未登录返回没有权限
return simpleAuthorizationInfo;
}
Claims claims = TokenUtil.parseJWT(token);
String userName = (String) claims.get("userName");
String resListIsNotAdmin = (String) claims.get("resList");
//获取角色有的资源
String[] arr = resListIsNotAdmin != null && StringUtils.isNotEmpty(resListIsNotAdmin) ? resListIsNotAdmin.split(",") : null;
if(arr == null || arr.length == 0){
return simpleAuthorizationInfo;
}
for(String res : arr){
simpleAuthorizationInfo.addStringPermission(res);
}
return simpleAuthorizationInfo;
}
//用户认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//加这一步的目的是在Post请求的时候会先进认证,然后在到请求
String token = (String) auth.getPrincipal();
if (token == null) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, token, getName());
return simpleAuthenticationInfo;
}
}
七:接下来是登录和操作案例
1.控制层
/**
* 用户登录
* [userName, passWord]
* @return {@link Result}
* @throws
* @author 李庆伟
* @date 2020/4/26 15:19
*/
@PostMapping(value = "login",name = "user/login")
@ResponseBody
public Result login(String userName, String passWord){
String tokenId = userService.login(userName,passWord);
return Result.success(tokenId);
}
/**
* 添加用户
* [userName, passWord]
* @return {@link Result}
* @throws
* @author 李庆伟
* @date 2020/4/26 15:19
*/
@RequiresPermissions("user:add")
@PostMapping("add")
@ResponseBody
public Result add(@RequestParam(value = "userName", required = true)String userName,
@RequestParam(value = "nickName", required = true)String nickName,
@RequestParam(value = "tele", required = true)String tele){
User user = userService.add(userName,nickName,tele);
return Result.success(user);
}
2.接口
/**
* 登录
* [userName]
* @return {@link User}
* @throws
* @author 李庆伟
* @date 2020/4/23 13:36
*/
String login(String userName, String passWord);
/**
* 用户添加
* [userName, passWord]
* @return {@link User}
* @throws
* @author 李庆伟
* @date 2020/4/26 15:10
*/
User add(String userName, String nickName, String tele);
3.接口实现类
/**
* 用户登录
* [userName, passWord]
* @return {@link User}
* @throws
* @author 李庆伟
* @date 2020/4/23 13:37
*/
public String login(String userName, String passWord) {
if(StringUtils.isEmpty(userName) || StringUtils.isEmpty(passWord)){
throw new MyException(ResultEnum.USER_LOGIN_ERROR.getExpKey(), ResultEnum.USER_LOGIN_ERROR.getExpValue());
}
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("user_name",userName);
wrapper.eq("pass_word", Md5Util.md5(passWord));
List list = userMapper.selectList(wrapper);
if((list == null || list.size() != 1 ) && !userName.equals("admin")){
throw new MyException(ResultEnum.USER_LOGIN_ERROR.getExpKey(), ResultEnum.USER_LOGIN_ERROR.getExpValue());
}
User user = new User();
//如果是管理员有全部权限
if(userName.equals("admin") && passWord.equals("admin")){
user.setId("admin");
user.setNickName("我是管理员");
user.setUserName("admin");
user.setPassWord(Md5Util.md5("admin"));
user.setTele("66666666666");
//特殊逻辑,管理员应该有所有权限,这里暂时没有写,只模拟了非管理员的情况
} else {
user = list.get(0);
List roleList = new ArrayList();
Role role = new Role();
role.setId("1");
role.setRoleName("我是超级管理员");
roleList.add(role);
user.setRoleList(roleList);
List resList = new ArrayList();
Res res1 = new Res();
res1.setId("11");
res1.setResCode("user:show");
resList.add(res1);
Res res2 = new Res();
res2.setId("12");
res2.setResCode("user:add");
resList.add(res2);
user.setResList(resList);
}
Subject subject = SecurityUtils.getSubject();
String token = TokenUtil.createJWT(user);
JwtToken jwtToken = new JwtToken(token);
subject.login(jwtToken);
return token;
}
/**
* 用户添加
* [userName, passWord]
* @return {@link User}
* @throws
* @author 李庆伟
* @date 2020/4/26 15:11
*/
public User add(String userName, String nickName, String tele) {
//添加用户前,判断用户名是否重复
Map map = new HashMap();
map.put("user_name",userName);
if(StringUtils.isNotEmpty(userName) && userName.equals("admin")){
throw new MyException(ResultEnum.USER_ADD_REPEAT.getExpKey(), ResultEnum.USER_ADD_REPEAT.getExpValue());
}
List list = userMapper.selectByMap(map);
if(list != null && list.size() > 0){
throw new MyException(ResultEnum.USER_ADD_REPEAT.getExpKey(), ResultEnum.USER_ADD_REPEAT.getExpValue());
}
User user = new User();
user.setUserName(userName);
user.setPassWord(Md5Util.md5("1"));
user.setNickName(nickName);
user.setTele(tele);
user.setCreateTime(new Date());
userMapper.insert(user);
return user;
}
到此文章结束。。。。。。。。。