文章目录
- token类
- 自定义realm
- 自定义的jwtFilter用于访问拦截:
- shiroconfig
- controller
- vo对象
- 测试
jwt和shiro框架就不多介绍了,直接上实例代码吧。目前测试可正常登录、获得用户角色、访问接口时根据@requireRoles进行拦截。
token类
import org.apache.shiro.authc.AuthenticationToken;
//一般的登陆只需要校验账号和密码两个要素,默认的UsernamePasswordToken就能满足需求
//这个就类似UsernamePasswordToken
//在JwtRealm中的授权部分,可以使用JwtUtil.decode(jwt).get("username")获取到username,使用username去数据库中查找到对应的权限,然后将权限赋值给这个用户就可以实现权限的认证了
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;
}
}
自定义realm
@Slf4j
public class JwtRealm extends AuthorizingRealm {
/*
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
* */
@Override
public boolean supports(AuthenticationToken token) {
//这个token就是从过滤器中传入的jwtToken
return token instanceof JWTToken;
}
@Autowired
private IInowRoleUserService roleUserService;
@Autowired
private IInowRoleService roleService;
@Autowired
private IInowUserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("=========执行了授权===========================");
InowUser userName = (InowUser) principalCollection.getPrimaryPrincipal();
InowUser inowUser = userService.selectOneByName(userName.getUserName());
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
final InowRoleUser roleuser = roleUserService.getOne(new LambdaQueryWrapper<InowRoleUser>().eq(InowRoleUser::getUserId, inowUser.getUserId()));
List<String> inowRoleList = new ArrayList<>();
inowRoleList.add(roleService.getOne(new LambdaQueryWrapper<InowRole>().eq(InowRole::getId,roleuser.getRoleId())).getRoleName());
simpleAuthorizationInfo.addRoles(inowRoleList);
//添加用户角色,用于@requireroles注解
return simpleAuthorizationInfo;
}
//认证
// 这里的 token是从 JWTFilter 的 executeLogin 方法传递过来的(请求头的token)
// 只要调用了subject.login(token)方法,就会进入到realm的doGetAuthenticationInfo内。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String)auth.getCredentials();
log.info("从auth获得的Token"+token);
String userName = JWTUtil.getUsername(token);
//用户不存在(这个在登录时不会进入,只有在token校验时才有可能进入)
if(userName == null)
{
throw new UnknownAccountException();
}
InowUser user = userService.selectOneByName(userName);
if(Objects.isNull(user))
{
throw new UnknownAccountException();
}
String salt = "666666"; //自己随便写的一个加密盐
Md5Hash md5Hash = new Md5Hash(user.getUserId(), salt, 1024);
if(!JWTUtil.verify(token,userName, md5Hash.toHex()))//学号当做用户密码
{
throw new IncorrectCredentialsException();
}
//toke过期
if(JWTUtil.isExpire(token)){
throw new ExpiredCredentialsException();
}
return new SimpleAuthenticationInfo(user, token, getName());
}
}
自定义的jwtFilter用于访问拦截:
@Slf4j
//@Component("jwtFilter")
//去掉@Component注解,让filter不交给springbean管理,因为这会导致shiro内置的anon过滤器失效,具体原因还未找出
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 进行token的验证
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
//在请求头中获取token
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization"); //前端命名Authorization
//token不存在
if(token == null || "".equals(token)){
Result result = new Result();
result.setCode(400);
result.setMsg("token为空,无法访问");
out(response,result);
return false;
}
//token存在,进行验证
JWTToken jwtToken = new JWTToken(token);
try {
SecurityUtils.getSubject().login(jwtToken); //通过subject,提交给myRealm进行登录验证
return true;
} catch (ExpiredCredentialsException e){
Result result = new Result();
result.setCode(400);
result.setMsg("登录已经过期");
out(response,result);
e.printStackTrace();
return false;
} catch (ShiroException e){
// 其他情况抛出的异常统一处理,由于先前是登录进去的了,所以都可以看成是token被伪造造成的
Result res = new Result();
res.setMsg("无效token");
out(response,res);
e.printStackTrace();
return false;
}
}
/**
* json形式返回结果token验证失败信息,无需转发
*/
private void out(ServletResponse response, Result res) throws IOException {
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
ObjectMapper mapper = new ObjectMapper();
String jsonRes = mapper.writeValueAsString(res);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
httpServletResponse.getOutputStream().write(jsonRes.getBytes());
}
/**
* 过滤器拦截请求的入口方法
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response); //token验证
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* isAccessAllowed()方法返回false,即认证不通过时进入onAccessDenied方法
*/
// @Override
// protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// return super.onAccessDenied(request, response);
// }
/**
* token认证executeLogin成功后,进入此方法,可以进行token更新过期时间
*/
// @Override
// protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
// }
}
shiroconfig
@Configuration
public class ShiroConfig {
public logOutFilter mylogoutFilter()
{
logOutFilter MylogoutFilter = new logOutFilter();
MylogoutFilter.setLoginUrl("login");//设置退出后重定向的跳转地址
return MylogoutFilter;
}
/**
* 注入Shiro过滤器链配置
* 注入安全服务配置
* 注入自定义jwt过滤器
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Autowired @Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager,
@Autowired ShiroFilterChainDefinition definition
){
JwtFilter jwtFilter = new JwtFilter();
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(defaultWebSecurityManager);//设置安全管理器
//可以添加shiro的内置过滤器
//anon:无需认证就可以访问 authc:必须认证了才能用 oerms:拥有对某个资源的权限才能访问
bean.setLoginUrl("/login");//设置登录路径
//bean.setUnauthorizedUrl("/unlogin");
Map<String, Filter> filterMap = new HashMap<>();
// 注销成功,则跳转到指定页面
filterMap.put("logout", mylogoutFilter());
filterMap.put("anon", new AnonymousFilter());
filterMap.put("jwt",jwtFilter);
bean.setFilters(filterMap);
bean.setFilterChainDefinitionMap(definition.getFilterChainMap());
return bean;
}
/**
* 自定义jwt过滤器
*
* @return
*/
//@Bean
public JwtFilter jwtFilter(){
return new JwtFilter();
}
/**
* 定义拦截器链,所有请求都经过自定义的jwt过滤器
*
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
Map<String,String> map = new LinkedHashMap<>();
map.put("/login","anon");
map.put("/swagger-ui.html","anon");
map.put("/doc.html","anon");
map.put("/**","jwt");
definition.addPathDefinitions(map);
return definition;
}
/**
* 自定义Realm
*
* @return
*/
@Bean
public JwtRealm myUserRealm(){
JwtRealm userRealm = new JwtRealm();
return userRealm;
}
/**
* 开启认证授权注解
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Autowired DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
//DefaultAdvisorAutoProxyCreator:
//BeanPostProcessor实现,它根据当前BeanFactory中的所有候选Advisor创建 AOP 代理。
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制指定注解的底层实现使用 cglib 方案,防止重复代理和可能引起代理出错的问题
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 配置安全服务机制,注入自定义Realm,关闭了shiro的默认session机制
*
* @param
* @return
*/
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
/*
* a. 告诉shiro不要使用默认的DefaultSubject创建对象,因为不能创建Session
* */
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultSubjectFactory();
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(@Autowired JwtRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
// 关闭 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//禁止Subject的getSession方法
securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
}
controller
@ApiOperation("登录接口")
@GetMapping
public Result userLogin(@ApiParam(name="userId",value="用户学号",required=true)
@RequestParam(value = "userId",required = true)String userId,
@ApiParam(name="userName",value="用户姓名",required=true)
@RequestParam(value = "userName",required = true)String userName) {
// 1、获取Subject实例对象
//根据用户名获取正确用户信息
InowUser user = userService.selectOneByName(userName);
if(user == null)
{
return Result.fail("无效用户,请检查您的姓名");
}
//盐 + 输入的密码(注意不是用户的正确密码) + 1024次散列,作为token生成的密钥
String salt = "666666";
Md5Hash md5Hash = new Md5Hash(userId, salt, 1024);
//生成token字符串
String token = JWTUtil.getJwtToken(userName, md5Hash.toHex()); //toHex转换成16进制,32为字符
log.info("从controller生成的Token"+token);
JWTToken jwtToken = new JWTToken(token);
Subject currentUser = SecurityUtils.getSubject();
// 4、认证
try {
currentUser.login(jwtToken);// 传到Realm类中的方法进行认证 进入 AuthenticationInfo方法
//session.setAttribute("username", userName);
InowUser nowUser = (InowUser) currentUser.getPrincipal();
InowUserVo userVo = new InowUserVo();
BeanUtils.copyProperties(nowUser,userVo);
userVo.setToken(token);
return Result.success(userVo);
}
catch (AuthenticationException e) {
String msg = null;
if (StringUtils.isNotEmpty(e.getMessage())) {
msg = e.getMessage();
}
return Result.fail("用户名与学号不匹配!");
}
// }
}
vo对象
在vo中放了token属性的成员属性
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class InowUserVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 学号或者教师id */
@NotBlank(message = "id不能为空!")
@ApiModelProperty(value="用户id,学号或教师号",name="id",example="2020214283")
private String userId;
private Integer id;
/** 姓名 */
@NotBlank(message = "姓名不能为空!")
@ApiModelProperty(value="用户姓名",name="userName",example="张三")
private String userName;
/** 学院 */
@ApiModelProperty(value="用户学院",name="userAcademy",example="计算机与信息学院")
private String userAcademy;
/** 微信id */
@ApiModelProperty(value="微信id",name="wechatId",example="微信id")
private Long wechatId;
@ApiModelProperty(value="性别",name="userSex",example="1男,2女")
private byte userSex;
@ApiModelProperty(value="最后登陆时间",name="loginDate")
private Date loginDate;
@ApiModelProperty(value="用户角色",name="roleName")
private String roleName;
private String token;
}
测试
登录接口:
访问一个标有注解@RequireRoles(“admin”)的方法:
在不加token时返回结果:
加一个没有Admin角色的Token访问结果:
用有admin角色的用户token访问结果: