现在大部分web应用都是使用前后端分离的架构开发,这就需要通过token做为前端访问后端接口时的身份标识。前端在访问登录接口时,登陆成功给前端返回一个token值,前端可以通过这个token值来访问被赋予权限的接口。
登录接口如下所示,在登录阶段,把用户输入的用户名和密码信息与后端数据库中所存储的信息进行比对,如果比对成功且符合登录条件,后端将会产生一个token给前端。
@GetMapping("/login")
public ResponseVo login(String username, String password){
if(StringUtils.isEmpty(username)){
return ResponseVo.error("未输入用户名");
}
SysUserEntity user=sysUserService.findByUserName(username);
if(user==null||!user.getPassword().equals(new Sha256Hash(password, user.getSalt()).toHex())){
return ResponseVo.error("用户不存在或密码不正确");
}
if(user.getStatus()==0){
return ResponseVo.error("用户已锁定,请联系管理员");
}
ResponseVo responseVo=sysUserTokenService.createToken(user.getUserId());
return responseVo;
}
ShiroConfig是shiro的配置类,在配置类中配置了安全管理器,shiro过滤器,生命周期后置处理器、授权资源通知器和默认通知器代理生成器。
1、AuthorizationAttributeSourceAdvisor和DefaultAdvisorAutoProxyCreator的配置是为了开启shiro注解(例如@RequiredPermissions、@RequiredRoles等)。
2、LifecycleBeanPostProcessor是用来管理shiro的生命周期。
3、shiro过滤器主要是来判定哪些接口需要认证,哪些接口可以匿名访问,由于使用了token方式,所以需要编写一个适用于token方式认证的过滤器。
4、SecurityManager做为shiro框架中的安全管理器,主要需要注入shiroRealm属性,来实现身份认证功能和授权功能。
@Configuration
public class ShiroConfig {
@Bean
public SecurityManager securityManager(ShiroRealm shiroRealm){
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(shiroRealm);
defaultWebSecurityManager.setRememberMeManager(null);
return defaultWebSecurityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
HashMap<String,Filter> map=new HashMap();
map.put("oauth2",new AuthenticFilter());
shiroFilterFactoryBean.setFilters(map);
HashMap<String,String> filter=new HashMap();
filter.put("/kaptcha.jpg","anon");
filter.put("/login","anon");
filter.put("/webjars/**","anon");
filter.put("/swagger/**","anon");
filter.put("/v2/api-docs", "anon");
filter.put("/swagger-ui.html", "anon");
filter.put("/swagger-resources/**", "anon");
filter.put("/**","oauth2");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filter);
return shiroFilterFactoryBean;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
}
上一小节提到,使用token方式登录需要自己编写接口的过滤器。该过滤器中一共重写了四个方法,分别是createToken、onAccessDenied、onLoginFailure和isAccessAllowed。
1、createToken是为了在后续的登录流程中产生一个AuthenticationToken对象,而这个对象是在前端请求时在Header或者Parameter中携带的(也就是在登录成功时,后端返回给前端的)。
2、onAccessDenied是拿到前端携带的token之后,进行shiro后续的身份认证流程。
3、onLoginFailure是在身份认证失败后进行的流程。
4、isAccessAllowed是允许不经过过滤器的条件。
public class AuthenticFilter extends AuthenticatingFilter {
public final static String TOKEN="token";
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
String token=getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
return null;
}
return new AuthenticToken(token);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String token=getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
HttpServletResponse httpReponse= (HttpServletResponse) response;
httpReponse.setHeader("Access-Control-Allow-Credentials","true");
httpReponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getHeader("Origin"));
httpReponse.getWriter().print(JSON.toJSONString(ResponseVo.error(HttpStatus.SC_UNAUTHORIZED,"invalid token")));
return false;
}
return executeLogin(request,response);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse= (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getHeader("Origin"));
try {
Throwable throwable=e.getCause();
//throwable.getMessage()
ResponseVo responseVo=ResponseVo.error(HttpStatus.SC_UNAUTHORIZED,null);
httpResponse.getWriter().print(JSON.toJSON(responseVo));
} catch (IOException e1) {
e1.printStackTrace();
}
return false;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(RequestMethod.OPTIONS.name().equals(((HttpServletRequest) request).getMethod())){
return true;
}
return false;
}
private String getRequestToken(HttpServletRequest request){
String token=request.getHeader(TOKEN);
if(StringUtils.isBlank(token)){
token=request.getParameter(TOKEN);
}
return token;
}
}
ShiroRealm重写了父类三个方法,分别是supports、doGetAuthorizationInfo和doGetAuthenticationInfo。
1、supports方法是为了让realm可以支持我们所自定义的token。在shiro框架登录的源码中,会对我们所传入的token进行判断,如果我们未重写该方法,那么登录将会失败。
2、doGetAuthenticationInfo,该方法是用来存储登录用户的身份认证信息。前端在登录成功后拿到后端返回的token。然后带着这个token去访问需要权限的接口时,会先经过shiro框架内部的登录流程,而登录流程的最终步骤就是调用doGetAuthenticationInfo这个方法,产生一个AuthenticationInfo类型的对象存储到当前的后台线程中,以供后续的授权服务使用。
3、doGetAuthorizationInfo,该方法是来存储用户的所拥有的授权信息。当前端所调用的方法上遇到@RequiredPermissions注解时,shiro框架就会根据我们通过该方法创建的AuthorizationInfo对象中去寻找登录用户是否拥有该权限。该方法的内容比较简单,就是通过doGetAuthenticationInfo产生的AuthenticationInfo类型的对象中的user信息从数据库中的role表中查出相对应的角色,然后拿着角色信息去权限表中查出所拥有的权限存储到AuthorizationInfo类型的对象中。
@Component
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserTokenRepository sysUserTokenRepository;
@Autowired
private SysUserRepository sysUserRepository;
@Autowired
private SysPermissionRepository sysPermissionRepository;
//realm必须支持接受token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof AuthenticToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
SysUserEntity user= (SysUserEntity) principals.getPrimaryPrincipal();
Set<String> permissionSet=new HashSet<>();
List<String> list=sysPermissionRepository.getPermsByUserId(user.getUserId());
for(String perms:list){
if(StringUtils.isBlank(perms)){
continue;
}
permissionSet.addAll(Arrays.asList(perms.trim().split(",")));
}
authorizationInfo.setStringPermissions(permissionSet);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken= (String) token.getPrincipal();
Optional<SysUserTokenEntity> userTokenOpt=sysUserTokenRepository.getSysUserTokenEntitiesByToken(accessToken);
if(!userTokenOpt.isPresent()){
throw new IncorrectCredentialsException("token不存在");
}
SysUserTokenEntity userToken=userTokenOpt.get();
if(userToken.getExpireTime().getTime()<System.currentTimeMillis()){
throw new IncorrectCredentialsException("token已经失效");
}
SysUserEntity user=sysUserRepository.getById(userToken.getUserId());
SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(user,accessToken,getName());
return authenticationInfo;
}
}
1.调用/login方法进行登录操作,后端返回token。
2.将该token放入请求的Header中,去访问/createUser接口(该登录用户拥有sys:user:save权限)
3.测试结果