SpringBoot+Shiro+Jwt
流程:
1、用户请求,不携带token,在JwtFilter处做判断,若没有token,则可以访问没有添加@RequiresAuthentication注解的方法,若有token,则如2所示
2、用户请求,携带token,就到JwtFilter中获取jwt,封装成JwtToken对象。然后使用JwtRealm进行认证
3、在JwtRealm中进行认证判断这个token是否有效,有效则可访问url
1、依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、创建JwtUtils
public class JwtUtils {
private static final String secret="f4e2e52034348f86b67cde581c0f9eb5";
private static final long expire=604800;
private static final String header="Authorization";
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
log.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
3、JwtFilter
Jwt拦截器,会拦截所有请求,对请求进行过滤,后续会注册到shrio的配置文件中
public class JwtFilter extends AccessControlFilter {
@Autowired
JwtUtils jwtUtils;
/**
* isAccessAllowed()判断是否携带了有效的JwtToken
* onAccessDenied()是没有携带JwtToken的时候进行账号密码登录,登录成功允许访问,登录失败拒绝访问
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
/**
* 1. 返回true,shiro就直接允许访问url
* 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
* 这里先让它始终返回false来使用onAccessDenied()方法
*/
log.warn("isAccessAllowed方法被调用");
return false;
}
/**
*
* @param servletRequest
* @param servletResponse
* @return返回结果为true表明登录通过
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
/**
*这个地方和前端约定,要求前端将jwtToken放在请求的Header部分
*所以以后发起请求的时候就需要在Header中放一个Authorization,值就是对应的Token
*/
log.warn("onAccessDenied方法被调用");
HttpServletRequest request = (HttpServletRequest)servletRequest;
String token = request.getHeader("Authorization");
if(token==null){//如果token为空的话,返回true,交给控制层@RequiresAuthentication进行判断;也会达到没有权限的作用
return true;
}
JwtToken jwtToken = new JwtToken(token);
try{
/**
* 进行登录处理
* 委托 realm 进行登录认证
* 所以这个地方最终还是调用AccountRealm进行的认证
*/
getSubject(servletRequest,servletResponse).login(jwtToken);
}catch (Exception e){
// e.printStackTrace();
// onLoginFail(servletResponse);
return false;
}
//如果走到这里,那么就返回true,代表登录成功
return true;
}
//登录失败要执行的方法
private void onLoginFail(ServletResponse response) throws IOException{
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.getWriter().print("login error");
}
}
4、创建JwtToken类
JwtToken类要继承AuthenticationToken类
//继承AuthenticationToken 要和AccountRealmh中的doGetAuthenticationInfo的参数类型保持一致
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String jwt){
this.token = jwt;
}
@Override//类似用户名
public Object getPrincipal() {
return token;
}
@Override//类似密码
public Object getCredentials() {
return token;
}
}
5、创建AccountRealm类
用来对用户进行认证和授权
/**
* //getName():返回一个唯一的Realm名字
*/
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
IUserService userService;
/**
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
*
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String jwt = (String)authenticationToken.getPrincipal();
String id = jwtUtils.getClaimByToken(jwt).getSubject();//获取jwt中关于用户的id
User user = userService.getById(Long.valueOf(id));//查询用户
if(user == null){
throw new UnknownAccountException("用户不存在");
}
if(user.getStatus()==-1){
throw new LockedAccountException("用户被锁定");
}
Claims claims = jwtUtils.getClaimByToken(jwt);
if(jwtUtils.isTokenExpired(claims.getExpiration())){
throw new ExpiredCredentialsException("token过期,请重新登录");
}
AccountProfile accountProfile = new AccountProfile();//新建一个用户类,这里包括需要认证的用户信息,当然也可以直接用user
BeanUtil.copyProperties(user,accountProfile);//将user的属性copy到accountProfile中,其实这里也可以用user
return new SimpleAuthenticationInfo(accountProfile,jwt,getName());
}
}
6、ShiroConfig
/**
* shiro启用注解拦截控制器
* shiro的三个重要配置
* 1、Realm
* 2、DefaultWebSecurityManager
* 3、ShiroFilterFactoryBean
*/
@Configuration
public class ShiroConfig {
@Autowired
JwtFilter jwtFilter;
// @Bean
// public SubjectFactory subjectFactory(){
// return new DefaultSubjectFactory();
// }
@Bean//把AccountRealm注入到spring容器中
public Realm realm(){
return new AccountRealm();
}
@Bean
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
//关闭shiroDao功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 ShiroSession 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
/**
* 添加jwt过滤器,并在下面注册
* 也就是将jwtFilter注册到shiro的Filter中
* 指定除了login之外的请求都先经过jwtFilter
*/
HashMap<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt",new JwtFilter());
shiroFilter.setFilters(filterMap);
/**
* 拦截器
*/
LinkedHashMap<String, String> filtRuleMap = new LinkedHashMap<>();
filtRuleMap.put("/login","anon");
filtRuleMap.put("/**","jwt");
shiroFilter.setFilterChainDefinitionMap(filtRuleMap);
return shiroFilter;
}
/**
* 下面的的bean是为了解决@RequiresAuthentication注解不生效的配置
*/
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new
DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new
AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
7、测试类
@RestController
public class AccountController {
@Autowired
JwtUtils jwtUtils;
@Autowired
IUserService userService;
@PostMapping("/login")
public Result login(@RequestBody @Validated LoginDto loginDto, HttpServletResponse response){
Result result = new Result();
User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
if(user==null){
return Result.fail("用户名不存在");
}
if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
return Result.fail("用户密码错误");
}
String token = jwtUtils.generateToken(user.getId());
response.setHeader("Authorization",token);
response.setHeader("Access-control-Expost-Headers","Authorization");
return Result.succ(MapUtil.builder().put("token",token).map());
}
@RequiresAuthentication//访问该接口就需要有jwt认证
@GetMapping("/logout")
public Result logout(){
SecurityUtils.getSubject().logout();//退出登录
return Result.succ(null);
}
}