Springboot+jwt+shiro

1 篇文章 0 订阅
1 篇文章 0 订阅

一. 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;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值